4.8 KiB
Research: Reaction Image Board v1
Phase: 0 — Pre-design research Date: 2026-05-02
The technology stack is fully specified in the constitution's tech stack table (§6), so this document records rationale and key decisions rather than exploratory research.
Decision 1: Project layout
Decision: Two top-level directories (api/, ui/) in a single
repository, each with its own dependency manifest and Dockerfile.
Rationale: Constitution §1 mandates separate deployable artifacts with separate dependency manifests. A monorepo with two independently buildable services satisfies this without requiring a separate repo per service. Docker Compose ties them together for local development (§7.1).
Alternatives considered:
- Separate repositories — rejected because it adds checkout/sync overhead for a solo project with no reason to deploy them independently yet.
- Single
src/with both projects — rejected because it would entangle dependency manifests, violating §2.1.
Decision 2: SQLAlchemy async + asyncpg driver
Decision: SQLAlchemy 2.x in async mode with the asyncpg driver.
Rationale: Constitution §6 specifies this explicitly. FastAPI's async request handlers benefit directly; asyncpg is the fastest PostgreSQL driver for Python async code. The async session is injected per-request via FastAPI's dependency system.
Alternatives considered: Synchronous SQLAlchemy + psycopg2 — rejected per constitution mandate.
Decision 3: Storage object key
Decision: SHA-256 hex digest of file bytes, no prefix, no extension
(e.g. a3f1...).
Rationale: Spec §3 specifies this explicitly. Content-addressed keys
are stable and human-inspectable in the bucket. The same key is stored in
the storage_key column on the image record, so reconstruction without DB
state is possible.
Alternatives considered: UUID key — rejected; UUID keys lose the content-addressing property and the deduplication shortcut.
Decision 4: Duplicate detection strategy
Decision: Hash the file bytes in the API process; query PostgreSQL for an existing record with that hash before any storage write.
Rationale: Constitution §4.3 mandates deduplication by SHA-256. Performing
the hash and DB check before writing to S3 avoids wasting storage bandwidth
and keeps the duplicate-response path (HTTP 200 + duplicate: true) cheap.
Alternatives considered:
- Hash at upload, always write to S3 then check — rejected because it wastes S3 bandwidth for duplicate uploads.
- Client-side hash — rejected because the constitution places no trust in the client and the UI knows nothing about storage implementation (§2.3).
Decision 5: Tag validation pattern
Decision: ^[a-z0-9_-]{1,64}$ applied after normalisation (lowercase + trim).
Rationale: Spec §2.8 specifies this pattern exactly. Normalisation happens
before validation so that user input like " Cat " becomes "cat" and passes.
Decision 6: Pre-signed URL strategy
Decision: Generate a 1-hour pre-signed URL on each request to
GET /api/v1/images/{id}/file and return a 302 redirect.
Rationale: Spec §2.4 specifies this approach. The client (browser or Angular app) loads the image directly from S3/MinIO, avoiding API proxying of potentially large files. The 1-hour expiry is short enough to be meaningless at personal-use scale.
Alternatives considered:
- Proxy image bytes through the API — rejected because it wastes API memory and bandwidth for potentially large files.
- Permanent public S3 URLs — rejected because it exposes the bucket structure and requires the bucket to be public.
Decision 7: Test database and storage in integration tests
Decision: Integration tests run against a real PostgreSQL database and a real MinIO instance started by Docker Compose (or a dedicated test compose file). No mocking of the database or storage layer.
Rationale: Constitution §5.2 mandates "API routes tested against a real (test) database and a real (test) S3-compatible bucket (e.g. MinIO in Docker)". Mocking at this layer has historically caused test/prod divergence.
Alternatives considered:
- SQLite in-memory for integration tests — rejected; constitution mandates PostgreSQL specifically for the repository layer.
- Mocked S3 (moto) — rejected per constitution §5.2.
Decision 8: Angular tag filter debounce
Decision: Debounce tag filter bar API calls in LibraryComponent using
RxJS debounceTime (e.g. 300 ms).
Rationale: Spec §4.1 says "updates in real time (debounced API call)". 300 ms is a standard UX debounce that prevents excessive API calls while still feeling responsive.
All NEEDS CLARIFICATION resolved
No unresolved clarifications remain. The constitution and spec together fully specify the technology choices and behaviour for v1.