Aligns principles with actual project state: soften TDD wording to allow tests alongside implementation, replace CI gate with concrete local test suite gate, add production infrastructure to tech stack (k3s, nginx, Vault + VSO), and document plaintext password storage as a known gap that must be resolved before further auth work. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
14 KiB
Reaction Image Board — Project Constitution
This document is the authoritative source of project-wide principles. Every spec, plan, and task produced for this project MUST comply with it. Contradictions between a spec/plan/task and this constitution are resolved in favour of the constitution. Changes to the constitution require an explicit revision with a noted rationale.
1. Project Identity
A personal, self-hosted web application for uploading, tagging, and searching reaction images. The system is split into two independently deployable units:
- API — Python / FastAPI
- UI — Angular (standalone SPA, communicates with the API over HTTP)
There is no server-side rendering. The UI is a pure client that consumes the API. These two units may live in the same repository but are treated as separate deployable artifacts with separate dependency manifests.
2. Architecture Principles
2.1 Strict separation of concerns
The API knows nothing about the UI framework. The UI knows nothing about storage or database implementation details. All cross-unit communication is through versioned HTTP contracts (OpenAPI spec is the source of truth).
2.2 Dependency direction
UI → API → Storage (S3-compatible)
→ Database (PostgreSQL)
No layer MAY import or directly call a layer above it in this diagram.
2.3 Storage abstraction
All file I/O goes through a StorageBackend interface. The initial
implementation is S3-compatible (MinIO locally, real S3 in production).
No code outside the storage module MAY reference bucket names, S3 URLs,
or SDK-specific types directly — only the interface contract.
2.4 Auth abstraction (progressive)
Authentication is treated as a pluggable backend from day one. The API MUST
route all request-identity resolution through a single AuthProvider interface.
Each phase introduces a new provider implementation; no phase rewrites business
logic already behind the interface.
Phase 1 — no-auth (NoOpAuthProvider): complete. Phase 2 — JWT bearer token (JWTAuthProvider, HS256, single owner): complete. Phase 3 — OIDC: planned. The constitution acknowledges all three; the spec governs which is built.
2.5 Database abstraction
PostgreSQL is the database. All DB access MUST go through a repository layer (one repository class per domain aggregate). Raw SQL or an ORM is acceptable, but no query logic MAY live outside a repository. No alternative database engine (SQLite, DuckDB, in-memory substitutes) MAY be used in integration tests — dialect differences mask production bugs.
2.6 No speculative abstraction
Interfaces are introduced only when a second implementation either exists or is explicitly planned in the constitution (StorageBackend, AuthProvider, repositories). Everything else is concrete until a real second implementation is needed. "Just in case" generics, base classes, or plugin systems are prohibited.
3. API Design
3.1 Versioning
All API routes MUST be prefixed /api/v1/. Breaking changes require a new
version prefix. Adding fields to responses is non-breaking; removing or
renaming is breaking.
3.2 OpenAPI as contract
The FastAPI-generated OpenAPI schema is the binding contract between API and UI. The UI MUST NOT depend on behaviour not described in the schema.
3.3 Error shape
All error responses MUST use a consistent envelope:
{ "detail": "<human-readable message>", "code": "<machine-readable code>" }
The code field is required. Naked HTTP status codes with empty bodies are
prohibited.
3.4 Pagination
Any endpoint that returns a list MUST support cursor- or offset-based pagination from the start. No endpoint MAY return an unbounded list.
4. Data Model Principles
4.1 Tags are lowercase, normalised strings
Tags are stored and matched case-insensitively. The API MUST normalise tag input to lowercase and strip whitespace before persistence. The UI may display tags in any case but MUST send them lowercase.
4.2 Images are immutable after upload
Once an image is stored, its file content is never replaced. The stored object MUST NOT be mutated.
4.3 Deduplication by content hash
Every image MUST be hashed (SHA-256) on upload before storage. If a hash already exists in the database, the upload is rejected or the existing record is returned — the spec will decide the UX — but no duplicate object is written to storage. The hash is stored on the image record and may also serve as the storage object key.
4.4 Tag search is AND logic in v1
A search for tags [cat, funny] returns only images that have both tags.
OR/NOT logic is explicitly out of scope until the constitution is revised.
5. Testing Discipline
5.1 Tests are required alongside every implementation task
Every implementation task MUST be accompanied by tests covering its behaviour. The ideal is red-green-refactor: write a failing test, then make it pass. In practice, tests written in the same task as the implementation are acceptable; what is non-negotiable is that no implementation task is marked done without corresponding test coverage. Tasks MUST NOT be split such that implementation is complete but tests are deferred to a later task.
5.2 Test pyramid
- Unit tests — pure logic, repository mocks, no I/O
- Integration tests — API routes tested against a real PostgreSQL instance and a real S3-compatible bucket (e.g. MinIO in Docker). SQLite and other in-memory database substitutes are prohibited — PostgreSQL-specific behaviour (GROUP BY enforcement, JSON operators, constraint handling) MUST be exercised by the test suite.
- E2E tests — Angular + API, minimal set covering the core happy paths
Unit and integration tests are required. E2E tests are best-effort in v1.
5.3 Tests live next to the code they test
API tests in api/tests/, UI tests colocated with their components. No
separate top-level tests/ directory that mirrors the source tree.
5.4 The test suite must pass before any task is considered done
"Done" means: all tests pass, linter passes, type checker passes. A task MUST NOT be marked complete while any of these are failing.
The acceptance gate is make test-unit && make test-integration plus ruff check / ruff format --check for the API. A formal CI pipeline is planned
but not yet in place; until one exists, passing the above commands locally is
the required gate. When CI is introduced it MUST enforce the same checks.
6. Tech Stack Constraints
| Concern | Choice | Rationale |
|---|---|---|
| API language | Python 3.12+ | Primary language, type hints required |
| API framework | FastAPI | Async, OpenAPI-native |
| ORM / query | SQLAlchemy 2.x (async) + asyncpg driver | Repository layer owns all queries |
| DB migrations | Alembic | Schema changes tracked in version control |
| Object storage | S3-compatible via boto3 / aiobotocore |
Swap MinIO ↔ S3 via env config |
| Auth tokens | PyJWT (HS256) | Lightweight; compatible with OIDC migration path |
| UI framework | Angular (latest stable) | Job-relevant, learning goal |
| UI language | TypeScript strict mode | No any, no implicit types |
| Containerisation | Docker + Docker Compose | Local dev must start with one command |
| Production runtime | k3s (Kubernetes) | Manifests in k8s/; see deployment docs |
| Ingress | nginx ingress controller + cert-manager | TLS via Let's Encrypt (letsencrypt-prod ClusterIssuer) |
| Secret management | HashiCorp Vault + VSO (Vault Secrets Operator) | Secrets never committed; VSO syncs Vault KV v2 → K8s Secrets |
7. Developer Experience
7.1 One-command local start
docker compose up MUST start the full stack: PostgreSQL, MinIO, API, UI dev
server. No manual environment setup beyond copying .env.example.
7.2 Environment configuration
All environment-specific values (DB URL, S3 endpoint, bucket name, ports) MUST
come from environment variables. Hardcoded hostnames, credentials, or ports in
source code are prohibited. .env.example is committed; .env is gitignored.
7.3 Linting and formatting are not optional
API: ruff (lint + format). UI: eslint + prettier. Both are enforced in
CI. PRs/commits with lint failures MUST NOT be considered complete tasks.
8. Scope Boundaries (v1)
The following are explicitly out of scope for v1 and MUST NOT be designed for, abstracted toward, or mentioned in specs unless the constitution is revised:
- Multi-user support
- Public sharing or embeds
- Collections or albums beyond tag-based grouping
- Image editing or transformation beyond thumbnail generation
- OR/NOT tag logic
- Mobile-native app
- OIDC auth (planned Phase 3)
Known gaps carried forward from v1 — these are not out of scope; they are acknowledged deficiencies that MUST be resolved before the affected area is expanded:
- Password hashing: The owner password is currently stored and compared in plaintext. Hashing (bcrypt or Argon2) MUST be implemented before any additional authentication work (e.g. OIDC, additional accounts) is started. Specs that touch credential storage MUST address this first.
9. Governance
This constitution supersedes all other project practices, conventions, and prior documentation. Where a conflict exists between a spec, plan, or task and this constitution, the constitution prevails.
Amendment procedure: Any change to this document MUST be accompanied by:
- An explicit rationale entry in the Revision Log (§10).
- A version bump following semantic versioning (see below).
- A review of all active specs and plans for compliance with the change.
Versioning policy:
- MAJOR — backward-incompatible governance change: a principle removed, redefined in a way that invalidates prior work, or a hard scope boundary moved inward.
- MINOR — new principle or section added, or materially expanded guidance that requires new compliance work.
- PATCH — clarifications, wording improvements, typo fixes, non-semantic refinements that do not change what is required.
Compliance review: Every new spec MUST include a "Constitution Check" gate (verified by the plan command) before Phase 0 research begins, and again after Phase 1 design is complete.
10. Revision Log
| Version | Date | Change |
|---|---|---|
| 1.0.0 | 2026-05-01 | Initial constitution |
| 1.1.0 | 2026-05-01 | asyncpg driver explicit; SHA-256 deduplication added to data model; deduplication removed from out-of-scope |
| 1.1.0 | 2026-05-02 | Adopted into Spec Kit memory; fixed duplicate §4.3 → §4.4; strengthened "should" language to MUST/MUST NOT; added §9 Governance |
| 1.1.1 | 2026-05-03 | Clarify that the only acceptable form of image transformation or editing is thumbnail generation |
| 1.2.0 | 2026-05-03 | §2.4: Mark Phase 2 (JWT bearer auth) complete, reword phase status; §6: Add PyJWT to tech stack table; §8: Remove username/password auth from out-of-scope (now shipped) |
| 1.3.0 | 2026-05-06 | §2.5: Remove planned PostgreSQL → SQLite refactor note; prohibit alternative database engines in integration tests. §5.2: Explicitly require PostgreSQL for integration tests; prohibit SQLite — a production HAVING/GROUP BY bug was masked by SQLite's permissive dialect. |
| 1.4.0 | 2026-05-08 | §5.1: Soften strict TDD wording to reflect actual practice — tests alongside implementation are acceptable; deferring tests to a later task is not. §5.4: Replace "CI must pass" with local test suite gate; note CI is planned but not yet in place. §6: Add production runtime rows (k3s, nginx ingress + cert-manager, Vault + VSO). §8: Add "known gaps" subsection; document plaintext password storage as a deficiency that must be resolved before further auth work. |
Version: 1.4.0 | Ratified: 2026-05-01 | Last Amended: 2026-05-08