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