Personal collection management API for tracking antiques, art, and valuables.
| Component | Technology |
|---|---|
| Framework | FastAPI |
| Database | PostgreSQL (async via asyncpg) |
| ORM | SQLAlchemy 2.0 |
| Migrations | Alembic |
| Auth | FastAPI-Users (JWT) |
| Resend | |
| Package Manager | uv |
| Containerization | Docker / Docker Compose |
| Testing | Pytest (async) |
| Linting/Formatting | Ruff |
- Python 3.12+
- uv
- Docker & Docker Compose (for local development)
# Clone and enter directory
cd trove-api
# Copy environment file
cp .env.example .env
# Install dependencies
uv sync
# Start PostgreSQL
docker compose up -d db
# Run migrations
uv run alembic upgrade head
# Start the API
uv run uvicorn app.main:app --reloadThe API will be available at http://localhost:8000
# Start everything (API + PostgreSQL)
docker compose up
# Or run in background
docker compose up -dOnce running, visit:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- OpenAPI JSON: http://localhost:8000/openapi.json
| Method | Endpoint | Description |
|---|---|---|
| POST | /auth/register |
Create a new account |
| POST | /auth/jwt/login |
Get JWT access token |
| POST | /auth/jwt/logout |
Revoke the refresh token family and clear cookies |
| POST | /auth/refresh |
Exchange the refresh cookie for a new access token |
| GET | /auth/me |
Current user |
| PATCH | /auth/me |
Change own settings (preferred_units) |
| POST | /auth/forgot-password |
Email a password reset link (always 202) |
| POST | /auth/reset-password |
Consume a reset token and set a new password |
| POST | /auth/request-verify-token |
Email an address-confirmation link (always 202) |
| POST | /auth/verify |
Consume a verification token |
/auth/forgot-password and /auth/request-verify-token answer 202 whether or
not the address has an account, so neither reveals who is registered. Both
links land in the frontend (/reset-password, /verify-email), which posts the
token back to the endpoint above it. Tokens last one hour.
Verification is offered, not required: nothing in the API checks is_verified,
so registering grants immediate access.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /collections |
Yes | List user's collections |
| GET | /collections/{id} |
Yes | Get collection with item count |
| POST | /collections |
Yes | Create a collection |
| PATCH | /collections/{id} |
Yes | Update a collection |
| DELETE | /collections/{id} |
Yes | Delete a collection |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /items |
Yes | List items (supports filters) |
| GET | /items/{id} |
Yes | Get an item by ID |
| POST | /items |
Yes | Create an item |
| PATCH | /items/{id} |
Yes | Update an item |
| DELETE | /items/{id} |
Yes | Delete an item |
Item List Filters:
collection_id- Filter by collectioncategory- Filter by categorysearch- Search in name, description and reference number
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /categories |
Yes | List suggested + user's categories |
id- UUIDname- String (max 200)description- Text (optional)created_at,updated_at- Timestamps
- Basic Info: name, description, reference_number, condition, location
- Acquisition: acquisition_date, acquisition_method, acquisition_price, acquisition_source, acquisition_place
- Value: estimated_value (the owner's current figure), plus a history of valuations (
/items/{id}/valuations) - Provenance: artist_maker, origin, date_era
- Physical: height_cm, width_cm, depth_cm, length_cm, diameter_cm, weight_kg, materials
- Metadata: created_at, updated_at
Condition Options: excellent, good, fair, poor, unknown
Acquisition Methods: purchase, gift, inheritance, trade, commission, other
Dates that may be imprecise (acquisition_date, a valuation's valued_on) are ISO
8601 at the precision known: 1998, 1998-06 or 1998-06-15. Measurements are
stored metric; a user's preferred_units only changes how they are shown.
# Apply all pending migrations
uv run alembic upgrade head
# Rollback one migration
uv run alembic downgrade -1
# Generate new migration after model changes
uv run alembic revision --autogenerate -m "description"Tests use SQLite in-memory for speed and isolation.
# Run all tests
uv run pytest
# Run with verbose output
uv run pytest -v
# Run with coverage
uv run pytest --cov=app# Check for linting errors
uv run ruff check .
# Format code
uv run ruff format .trove-api/
├── app/
│ ├── auth/ # FastAPI-Users JWT setup
│ ├── models/ # SQLAlchemy models (User, Collection, Item)
│ ├── routers/ # API route handlers
│ ├── schemas/ # Pydantic request/response schemas
│ ├── config.py # Settings from environment
│ ├── database.py # Async SQLAlchemy setup
│ ├── email.py # Transactional email transport (Resend)
│ ├── email_templates.py # What those emails say
│ └── main.py # FastAPI app entry point
├── alembic/
│ ├── versions/ # Migration files
│ └── env.py # Alembic configuration
├── tests/
│ ├── conftest.py # Pytest fixtures
│ ├── test_collections.py
│ └── test_items.py
├── .env.example
├── docker-compose.yml
├── Dockerfile
└── pyproject.toml
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | postgresql+asyncpg://postgres:postgres@localhost:5432/trove_db |
SECRET_KEY |
JWT signing key | change-me-in-production |
ENVIRONMENT |
development or production |
development |
RESEND_API_KEY |
Resend API key. Empty disables sending (see below) | (empty) |
EMAIL_FROM |
Sender, RFC 5322 form | Trove <noreply@mail.trovebox.io> |
Leave RESEND_API_KEY empty. Nothing is sent, and the message — including the
reset link — is logged to stdout instead, so the whole forgot-password flow can
be walked through locally with no Resend account and no real inbox.
That only happens when ENVIRONMENT=development. Anywhere else an empty key
logs an error naming the recipient and the subject, and never the link: the link
is a working password reset token, and a log is the wrong place to keep one.
This project is licensed under the Apache 2.0 License - see the LICENSE file for details.