135 lines
4.8 KiB
Markdown
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.
|