Backend Phase A: foundation, migrations, Docker setup
- Go backend skeleton: config (caarlos0/env), slog JSON logging, pgxpool store, HTTP server with graceful shutdown. - httpapi: render helpers, Problem errors, middleware chain (requestID / logging / recover / CORS), /healthz and /readyz. - Migrations: full initial schema (users, sessions, lists, list_members, items, op_log SOURCE OF TRUTH, item_names) + golang-migrate runner binary using source/iofs (embedded). - Docker: multi-stage Dockerfile (Go 1.26 -> distroless nonroot), builds both server and migrate binaries. - deploy: docker-compose (caddy + backend + migrate + postgres:16), Caddyfile (auto-HTTPS), .env.example, pg extensions init script. - AGENTS.md: project context + roadmap for AI agents. Verified: image builds, both binaries run in container (smoke test).
This commit is contained in:
commit
2899eb205b
23 changed files with 1372 additions and 0 deletions
114
backend/migrations/000001_init_schema.up.sql
Normal file
114
backend/migrations/000001_init_schema.up.sql
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
-- Initial schema for mitbringsl.
|
||||
-- Design notes:
|
||||
-- * op_log is the append-only SOURCE OF TRUTH for sync.
|
||||
-- * users/lists/items are projections of op_log (kept up to date as ops arrive).
|
||||
-- * Tombstones (deleted_at) prevent resurrection of removed items by late offline ops.
|
||||
-- * (client_id, client_seq) unique key on op_log makes pushes idempotent.
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- users
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT, -- NULL for OIDC-only accounts
|
||||
oidc_subject TEXT, -- IdP subject id
|
||||
oidc_issuer TEXT, -- which IdP the subject belongs to
|
||||
display_name TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (oidc_issuer, oidc_subject)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- sessions (opaque tokens issued by this backend after login)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE, -- store SHA-256 hash, never the raw token
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
user_agent TEXT,
|
||||
ip TEXT
|
||||
);
|
||||
CREATE INDEX sessions_user_id_idx ON sessions(user_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- lists (projection)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE lists (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ, -- tombstone
|
||||
hlc_ts BIGINT NOT NULL DEFAULT 0,
|
||||
version BIGINT NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX lists_owner_idx ON lists(owner_id) WHERE deleted_at IS NULL;
|
||||
|
||||
-- list_members (prepared for shared lists; MVP uses owner only)
|
||||
CREATE TABLE list_members (
|
||||
list_id UUID NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL DEFAULT 'member', -- 'owner' | 'member'
|
||||
PRIMARY KEY (list_id, user_id)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- items (projection)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
list_id UUID NOT NULL REFERENCES lists(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
quantity TEXT,
|
||||
checked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
sort_order INTEGER,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ, -- tombstone
|
||||
hlc_ts BIGINT NOT NULL DEFAULT 0,
|
||||
client_id UUID,
|
||||
checked_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX items_list_idx ON items(list_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX items_checked_idx ON items(list_id, checked) WHERE deleted_at IS NULL;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- op_log (SOURCE OF TRUTH)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE op_log (
|
||||
seq BIGSERIAL PRIMARY KEY, -- monotonic server cursor
|
||||
list_id UUID NOT NULL, -- list the op belongs to (FK added after lists exist; keep loose for tombstone cleanup)
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
client_id UUID NOT NULL,
|
||||
op_type TEXT NOT NULL, -- list_create|list_rename|list_delete
|
||||
-- item_add|item_update|item_remove
|
||||
target_id UUID NOT NULL, -- affected list/item id
|
||||
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
client_seq BIGINT NOT NULL, -- idempotency key part
|
||||
hlc_ts BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (client_id, client_seq)
|
||||
);
|
||||
CREATE INDEX op_log_list_seq_idx ON op_log(list_id, seq);
|
||||
CREATE INDEX op_log_target_idx ON op_log(target_id);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- item_names (autocomplete aggregation, all users)
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE item_names (
|
||||
name TEXT PRIMARY KEY, -- lowercased
|
||||
usage_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_used_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX item_names_trgm_idx ON item_names USING gin (name gin_trgm_ops);
|
||||
CREATE INDEX item_names_count_idx ON item_names(usage_count DESC);
|
||||
Loading…
Add table
Add a link
Reference in a new issue