Files
reactbin/specs/001-reaction-image-board/research.md

135 lines
4.8 KiB
Markdown

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