/feedback skeleton: first Posta participant inside posta-web #1

Closed
opened 2026-05-13 18:44:01 +02:00 by arne · 0 comments
Owner

What to build

Make `posta.no/feedback` a valid Posta participant. POST a signed envelope, get 204 + persisted message. GET with `Accept: application/posta+json`, get an actor doc advertising the room's key. No room behaviour yet (no subscribers, no fan-out) — that lands in a follow-up.

This is the slice that turns posta-web from a stateless static-site server into a stateful one.

Scope:

  • New `internal/store` package: SQLite via `modernc.org/sqlite` (CGO-free). Schema with a `messages` table (msg_id, sender, raw_envelope, signature, received_at), idempotent migration on startup. Implements `pkg/posta.MessageStore` (atomic insert against a uniqueness constraint on `(sender, msg_id)`; returns `*Error{ErrDuplicateID}` on conflict).
  • New `posta-web genkeys --out ` subcommand that writes a fresh Ed25519 keypair in the established `keys.json` format. Refuses to overwrite an existing file unless `--force`.
  • Mount `/feedback` on the existing posta-web HTTP mux: `pkg/posta.NewHandler` wired with the store, the keypair, and an `ActorDoc{Kind:"room", Name:"Posta — feedback", About:"…", Avatar:"https://posta.no/static/mark.svg"}`.
  • Bootstrap update for the persistent container: `/var/lib/posta-web/` directory with `posta:posta` ownership and 0700 mode; OpenRC service unit revised so the binary opens `feedback.db` and `feedback-keys.json` at startup.
  • Startup hook: on every start, reset any `outbound.status='sending'` rows to `pending` (fixes the posta-server landmine in advance).
  • Pre-seed the operator's own URL (`https://arne.posta.no`) into a `subscribers` table at first start. (Table created here; the column for permanent-failure tracking and the subscribe-on-POST logic come in the room-broadcasts slice.)

DEPLOY.md grows a section covering the new persistent volume.

Acceptance criteria

  • POST a signed envelope to `/feedback` returns `204` and persists `(envelope, raw_body, signature)`
  • GET `/feedback` with `Accept: application/posta+json` returns an actor doc with `kind:"room"` and the room's published key
  • Duplicate POSTs (same `sender` + `id`) return `409 duplicate-id`
  • `posta-web genkeys --out ` produces a valid `keys.json` and refuses overwrite without `--force`
  • Container start-up creates missing tables, applies pending migrations, and resets stuck `sending` rows
  • `/var/lib/posta-web/` exists in the container with correct ownership/mode; DEPLOY.md documents the volume and bootstrap order
  • The operator's own URL is in `subscribers` after first start (idempotent)

Blocked by

## What to build Make \`posta.no/feedback\` a valid Posta participant. POST a signed envelope, get 204 + persisted message. GET with \`Accept: application/posta+json\`, get an actor doc advertising the room's key. No room behaviour yet (no subscribers, no fan-out) — that lands in a follow-up. This is the slice that turns posta-web from a stateless static-site server into a stateful one. Scope: - New \`internal/store\` package: SQLite via \`modernc.org/sqlite\` (CGO-free). Schema with a \`messages\` table (msg_id, sender, raw_envelope, signature, received_at), idempotent migration on startup. Implements \`pkg/posta.MessageStore\` (atomic insert against a uniqueness constraint on \`(sender, msg_id)\`; returns \`*Error{ErrDuplicateID}\` on conflict). - New \`posta-web genkeys --out <path>\` subcommand that writes a fresh Ed25519 keypair in the established \`keys.json\` format. Refuses to overwrite an existing file unless \`--force\`. - Mount \`/feedback\` on the existing posta-web HTTP mux: \`pkg/posta.NewHandler\` wired with the store, the keypair, and an \`ActorDoc{Kind:\"room\", Name:\"Posta — feedback\", About:\"…\", Avatar:\"https://posta.no/static/mark.svg\"}\`. - Bootstrap update for the persistent container: \`/var/lib/posta-web/\` directory with \`posta:posta\` ownership and 0700 mode; OpenRC service unit revised so the binary opens \`feedback.db\` and \`feedback-keys.json\` at startup. - Startup hook: on every start, reset any \`outbound.status='sending'\` rows to \`pending\` (fixes the posta-server landmine in advance). - Pre-seed the operator's own URL (\`https://arne.posta.no\`) into a \`subscribers\` table at first start. (Table created here; the column for permanent-failure tracking and the subscribe-on-POST logic come in the room-broadcasts slice.) DEPLOY.md grows a section covering the new persistent volume. ## Acceptance criteria - [ ] POST a signed envelope to \`/feedback\` returns \`204\` and persists \`(envelope, raw_body, signature)\` - [ ] GET \`/feedback\` with \`Accept: application/posta+json\` returns an actor doc with \`kind:\"room\"\` and the room's published key - [ ] Duplicate POSTs (same \`sender\` + \`id\`) return \`409 duplicate-id\` - [ ] \`posta-web genkeys --out <path>\` produces a valid \`keys.json\` and refuses overwrite without \`--force\` - [ ] Container start-up creates missing tables, applies pending migrations, and resets stuck \`sending\` rows - [ ] \`/var/lib/posta-web/\` exists in the container with correct ownership/mode; DEPLOY.md documents the volume and bootstrap order - [ ] The operator's own URL is in \`subscribers\` after first start (idempotent) ## Blocked by - posta/spec#1 (needs \`ActorDoc.Kind\` and \`KeyFile\` reader)
arne closed this issue 2026-05-13 23:14:23 +02:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
posta/web#1
No description provided.