# 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.