Skip to content
IngwaldePublic

About

A web application for exploring and selecting existing color palettes for interior design projects.

Resources

Security policy

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

Palette v5.2 — Full-Stack Color Palette App

Live demo CI Python FastAPI PostgreSQL Docker License

Browse, search, save and export colour palettes. Palette is a solo portfolio project built to a production bar: httpOnly-cookie auth with CSRF, an async FastAPI backend on PostgreSQL, Redis-backed rate limiting, encrypted secrets, and continuous delivery to a live server.

Frontend → Fetch API → FastAPI Backend → PostgreSQL Database

Live demo: palettes-app.com — deployed on an Oracle Cloud VM behind Cloudflare + Caddy, auto-deployed on every green build to main. Full release history in CHANGELOG.md.


Screenshots

Palette page: the swatch grid with per-colour values, a colour-vision simulation control, fork/report actions and the WCAG contrast matrix

Home — the community feed Admin — colour-row editor Export — PNG preview
Home page: hero, search field and tag filter chips Admin panel: dynamic HEX-row colour editor with tag chips Export page: selected palette with PNG preview

Demo: live search filtering the palette grid down to matching palettes


Architecture

Full diagrams (ER, request path, auth flow) in docs/architecture.md.

Data model

erDiagram
    USERS ||--o{ FAVORITES : "saves"
    PALETTES ||--o{ FAVORITES : "saved in"
    USERS ||--o{ REFRESH_TOKENS : "owns"
    PALETTES }o..o{ TAGS : "referenced by name (JSONB)"

    USERS {
        int id PK
        string username UK
        string email UK
        string password_hash "Argon2id"
        bool is_admin
        int token_version "revokes issued access tokens"
        bool email_verified
    }
    PALETTES {
        int id PK
        string slug UK
        string name
        jsonb colors "list of HEX"
        jsonb tags "GIN-indexed"
    }
    TAGS {
        int id PK
        string name UK
        string kind "free | purpose"
    }
    FAVORITES {
        int id PK
        int user_id FK
        int palette_id FK
    }
    REFRESH_TOKENS {
        int id PK
        int user_id FK
        string token_hash UK
        bool revoked
    }
Loading

Request path (production)

flowchart LR
    U["Browser"] -->|HTTPS| CF["Cloudflare"]
    CF --> CA["Caddy (TLS, reverse proxy)"]
    CA -->|"/*"| FE["nginx — static frontend + CSP"]
    CA -->|"/api/*"| BE["FastAPI backend (async)"]
    BE --> DB[("PostgreSQL")]
    BE --> RD[("Redis — rate limiting")]
    BE -. "errors (if DSN)" .-> SN["Sentry"]
    U -. "errors + Web Vitals (if DSN)" .-> SN
Loading

Highlights

  • Cookie-based auth, done properly — JWT access + rotating refresh tokens in httpOnly/Secure cookies, double-submit CSRF on mutations, Argon2id hashing, server-side token revocation, and a login path that costs the same whether or not the account exists. Login by username or email.
  • Async request path — FastAPI with async SQLAlchemy 2.0 (asyncpg) on PostgreSQL 16; colors and tags stored as JSONB with a GIN index for fast tag filtering.
  • Hardened by default — Redis-backed rate limiting on every mutating endpoint, strict Content-Security-Policy and security headers, a CORS allowlist (never *), RFC 7807 errors, and a versioned, paginated /api/v1.
  • Secrets encrypted in git — SOPS + age; the plaintext .env is never committed and secrets are decrypted only on the deploy host.
  • Continuous delivery — a green CI run on main auto-deploys the exact commit CI validated, taking a database backup first because migrations apply on startup; isolated staging stack with its own secrets via a Compose override.
  • Operable in production — liveness/readiness probes, optional Sentry error tracking on both the backend and the browser frontend (client errors + Web Vitals), and a scripted database backup with retention.
  • Tested and linted — an async pytest suite with ruff, mypy and an 80% coverage gate; on the frontend, Vitest with a coverage gate, Playwright end-to-end and axe accessibility specs, screenshot baselines compared at zero tolerance, a performance budget, and an integration suite driving the real stack. All enforced in CI, alongside CodeQL analysis of this repository's own code and dependency audits on both ecosystems.

Built with

Layer Stack
Backend FastAPI · async SQLAlchemy 2.0 (asyncpg) · Pydantic v2 · Alembic · Argon2 · slowapi
Data PostgreSQL 16 (JSONB + GIN) · Redis
Frontend Vite · React 19 · TypeScript · React Router · TanStack Query
Infra Docker Compose · Caddy · Cloudflare · Oracle Cloud VM · SOPS + age
Styling vanilla-extract (typed, zero-runtime CSS-in-TS)
Quality / CI GitHub Actions · CodeQL · ruff · mypy · pytest · Vitest · Playwright + axe · Lighthouse CI · Sentry

Features

Auth & security

  • JWT access + rotating refresh tokens in httpOnly cookies with server-side revocation, plus double-submit CSRF on mutating requests.
  • Argon2id password hashing with pinned cost parameters (legacy PBKDF2 upgraded on next login), run off the event loop; timing-safe secret comparison; and a constant-cost login path, so response time does not reveal which accounts exist.
  • Login by username or email; email verification with a verify-link auto-login; password reset by email; self-service account deletion.
  • Redis-backed rate limiting (shared across instances) on auth and every mutating endpoint.
  • Security headers on both halves: nginx sets a strict CSP and the usual family for the SPA, and the API sets nosniff, DENY, a referrer policy and a default-src 'none' CSP on every response, error responses included. HSTS is sent only over https, so it cannot pin a browser during local development.
  • ALLOWED_HOSTS rejects requests for a Host the API does not answer for; explicit CORS allowlist; mandatory SECRET_KEY (fail-fast at startup).
  • Secrets encrypted in git via SOPS + age.

API & data

  • Versioned API under /api/v1; public palette API, auth API, favorites API, tag catalog.
  • Paginated palette list ({ items, total, limit, offset } + X-Total-Count), with SQL-side search, tag filtering, exact color counts (1–8) and sorting; colors/tags as JSONB with a GIN index on tags.
  • RFC 7807 application/problem+json error responses; Pydantic v2 validation.
  • Alembic migrations (safe adoption of a pre-Alembic database on startup); typed configuration via pydantic-settings; automatic seeding and first-admin creation from .env.
  • Swagger UI at /api/docs (off by default; enable with ENABLE_API_DOCS=true).

Ops & delivery

  • Liveness (/health) and readiness (/health/ready, checks database + Redis) probes; the Compose healthcheck uses readiness.
  • Continuous delivery: production auto-deploys the commit CI validated, after a database backup; isolated staging stack with its own encrypted secrets via a Compose override (see docs/deploy.md).
  • Optional Sentry error tracking — backend (off unless SENTRY_DSN is set) and frontend (client errors + Web Vitals, off unless VITE_SENTRY_DSN is set); scripted database backups with retention (see docs/ops.md).
  • Structured logging (LOG_LEVEL); automated tests with ruff, mypy and an 80% coverage gate in CI, plus pip-audit and npm audit on every run and weekly Dependabot updates.

Frontend

  • React Router client-side navigation with a sliding tab indicator that measures and follows the active link.
  • Search by name, description, slug and tags; combine tags, exact color counts (1–8) and sorting.
  • Color-first palette cards with per-color copy, Copy all, linked tags and account-only Save.
  • Guests who press Save return to their previous page after login; only that requested palette is saved. Registration preserves the request through email verification for up to 24 hours. Older on-device collections are no longer imported automatically.
  • Mobile navigation and theme switching are available to guests and signed-in users.
  • Save/remove favorites tied to the logged-in user; account page with password change; admin-only navigation hidden from guests.
  • Admin palette manager with a dynamic HEX-row colour editor and chip-based tag editing.
  • Export a selected palette as CSS, JSON or a standalone PNG palette card, with live preview. Detail-page export links preserve the owner and slug, including private palettes you can access.
  • Accessible touches: skip-to-content link, visible focus states, ARIA-labelled controls, toggle state exposed via aria-pressed; audited by axe in CI. Works over the LAN via a dynamic API base.
  • Styling is vanilla-extract — every rule scoped to the component or page that owns it, with design tokens typed in TypeScript. No global stylesheet beyond the document layer.

Quick start with Docker

Palette runs on PostgreSQL via Docker Compose — database, backend and static frontend together. This is the only supported way to run it (no SQLite / non-Docker mode).

docker compose up --build
  • Backend: http://localhost:8000 (Swagger at /api/docs when ENABLE_API_DOCS=true)
  • Frontend: http://localhost:5500
  • Database: PostgreSQL in the db service, data persisted in the pgdata volume.

backend/.env must exist (copy backend/.env.example and set a real SECRET_KEY). Compose sets DATABASE_URL automatically. Stop with docker compose down (add -v to drop the database volume).

Tests

Backend, against a disposable PostgreSQL in its own Compose profile:

docker compose --profile test run --rm tests

Frontend:

cd frontend-react
npm run test:coverage      # Vitest + coverage gate
npm run test:e2e           # Playwright flows, focus management + axe accessibility audit
npm run css:orphans        # class names in markup that no stylesheet defines
./scripts/visual.sh        # screenshot baselines, compared at zero tolerance
./scripts/integration.sh   # the same browser against the real Compose stack
./scripts/lighthouse.sh    # performance budget: asset sizes fail, timings warn

The stubbed E2E specs intercept every request, which is what makes them fast and what stops them noticing when the front end and the API disagree. integration.sh brings the whole stack up — nginx, FastAPI, PostgreSQL, Redis — runs against it, and tears it down with its volume; it needs ports 5500 and 8000 free, so a dev stack has to come down first.

The screenshots and the performance budget run inside a pinned Playwright container because rendering and timing are host-specific — a baseline recorded on Windows or macOS will never match CI. See frontend-react/README.md.


Development

Backend quality is enforced with ruff (lint + format), mypy and a pytest coverage gate — the same checks run in CI.

pip install -r backend/requirements-dev.txt
pre-commit install

ruff check backend/app backend/tests     # lint
ruff format backend/app backend/tests    # format
mypy backend/app                          # type-check

Schema changes use Alembic (backend/alembic/); the app runs migrations on startup and adopts an existing pre-Alembic database safely. Add a migration with:

docker compose run --rm backend alembic revision -m "describe change"

Project structure

Palette/
├── frontend-react/
│   ├── src/                  # api, auth, components, pages, lib, styles (*.css.ts)
│   ├── e2e/                  # Playwright: flows, a11y, focus · visual/ · integration/ · screenshots/
│   ├── scripts/              # visual.sh, integration.sh, lighthouse.sh, screenshots.sh
│   └── Dockerfile            # multi-stage build, served by nginx
├── backend/
│   ├── app/                  # main, config, database, models, schemas, security, routers/, crud
│   ├── alembic/              # migrations
│   ├── tests/                # async pytest suite
│   └── requirements*.txt
├── docs/                     # architecture, deploy, ops, secrets, api, auth, database, setup…
├── scripts/                  # backup-db.sh, split-secrets.sh
├── .github/workflows/        # ci.yml, deploy.yml, codeql.yml
├── docker-compose.yml        # + docker-compose.prod.yml, docker-compose.staging.yml
├── secrets/                  # SOPS-encrypted prod + staging env (safe to commit)
├── CHANGELOG.md · ROADMAP.md

Environment variables

Copy backend/.env.example to backend/.env and fill it in:

SECRET_KEY=change-this-secret-key-before-sharing
ACCESS_TOKEN_EXPIRE_MINUTES=1440
LOG_LEVEL=INFO
ENABLE_API_DOCS=false
CORS_ORIGINS=http://localhost:5500,http://127.0.0.1:5500
# DATABASE_URL is mandatory (PostgreSQL). Docker Compose sets it from the values below.
POSTGRES_USER=palette
POSTGRES_PASSWORD=palette
POSTGRES_DB=palette
DEFAULT_ADMIN_USERNAME=admin
DEFAULT_ADMIN_EMAIL=admin@palette.local
DEFAULT_ADMIN_PASSWORD=change-this-admin-password
# Email verification (Resend). Without RESEND_API_KEY the app logs the link instead.
RESEND_API_KEY=
EMAIL_FROM=Palette <noreply@palettes-app.com>
PUBLIC_BASE_URL=http://localhost:5500

SECRET_KEY is mandatory — the backend refuses to start if it is missing or left as a placeholder. Generate one with:

python -c "import secrets; print(secrets.token_urlsafe(48))"

CORS_ORIGINS is a comma-separated allowlist of browser origins (never *). Do not commit backend/.env — it is git-ignored; production and staging secrets live encrypted under secrets/ (see docs/secrets.md).


API overview

GET    /api/v1/palettes            # paginated, ?search= ?tag= ?sort= ?limit= ?offset=
GET    /api/v1/tags
POST   /api/v1/auth/register
POST   /api/v1/auth/login
POST   /api/v1/auth/refresh
POST   /api/v1/auth/logout
GET    /api/v1/auth/me
DELETE /api/v1/auth/me
GET    /api/v1/auth/verify?token=...
POST   /api/v1/auth/forgot-password
POST   /api/v1/auth/reset-password
PUT    /api/v1/auth/password
GET    /api/v1/favorites
POST   /api/v1/favorites/{slug}
DELETE /api/v1/favorites/{slug}

Admin palette/tag mutations require an authenticated admin (is_admin = true). GET /api/v1/palettes is paginated ({ items, total, limit, offset } + X-Total-Count). Errors are returned as application/problem+json (RFC 7807).


Roadmap

Release history is in CHANGELOG.md; the scope of each release, including what was deliberately deferred, is in ROADMAP.md.

Next up is v5.0 — the palette is the product. The 4.8.x line went almost entirely into infrastructure — security, CI, observability, accessibility — and the interface fell behind it: a filtered catalogue cannot be linked to, a single palette has no page of its own even though the API has served one since v3.0, and the grid stops at a hundred without saying so. v5.0 closes that gap: a shareable page per palette with a full contrast table, filters that live in the address bar, real pagination, a dark theme, and colour values in HEX, RGB, HSL or OKLCH.

The plan is twelve ordered steps, each with the files it touches, the interactions it has to produce and the tests it owes: docs/v5.0-plan.md.

Version

v5.2.8

License

Licensed under the PolyForm Noncommercial License 1.0.0 — you may use, modify and share this project for noncommercial purposes only. Commercial use is not permitted. This is a source-available license, not an OSI open-source license.

Copyright 2026 Ingwald.

Export and editing workflows (5.2.8)

Open Export palette from a palette to keep its exact owner/slug selection. The selected palette stays visible while Change palette opens search. Image groups PNG and SVG; Code groups CSS variables, CSS OKLCH, Tailwind configuration and JSON. Format/source changes live in the URL and support browser history. Image previews and downloads use the same generated content; code offers Copy code and Download file. Favorites exports use the authenticated account's complete favorites response. Guests can export public palettes, but cannot save favorites.

The shared editor starts with a live color preview. Use the arrow buttons to reorder colors, including by keyboard or touch; up to 8 six-digit HEX colors and 12 tags are supported. Invalid HEX remains in the field and prevents saving until corrected. Names require 2–160 characters; description is optional (up to 1000 characters). Failed saves retain the draft. Unsaved edits prompt before internal navigation or closing an admin editor, and use the browser's standard reload/close warning where supported. This is not autosave or crash recovery.

Admin Palettes opens with a compact list. New palette and Edit open a separate, nonmodal editor panel, which stacks above the list on narrow screens. Closing it returns focus to the initiating control. Search and pagination survive editing and tab switches. Tags and Reports retain their existing operations. Server authorization remains authoritative.

Local frontend checks: cd frontend-react, then npm ci, npm run build, npm run lint, npm run test:coverage. Browser E2E/a11y and visual-baseline commands remain in package.json; run them with the project's test backend and review screenshot differences before accepting.

About

A web application for exploring and selecting existing color palettes for interior design projects.

Resources

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages