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

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.