feat: use postgres

This commit is contained in:
Ilia Denisov
2026-04-26 20:34:39 +02:00
committed by GitHub
parent 48b0056b49
commit fe829285a6
365 changed files with 29223 additions and 24049 deletions
+66 -22
View File
@@ -63,38 +63,67 @@ Intentional omissions:
`cmd/userservice` loads config, constructs logging and telemetry, and then
creates the runtime through `internal/app.NewRuntime`.
The runtime wires:
The runtime wires, in order:
- Redis-backed stores for accounts, entitlement snapshots, sanctions, limits,
and listing indexes
- one shared `*redis.Client` opened through `pkg/redisconn` plus a Ping
- one PostgreSQL pool opened through `pkg/postgres`, instrumented with
`db.sql.connection.*` metrics, pinged, and migrated forward via the
embedded `internal/adapters/postgres/migrations` filesystem
- the PostgreSQL-backed user store from
`internal/adapters/postgres/userstore` (accounts, blocked-emails,
entitlement snapshot/history/lifecycle, sanction history/lifecycle,
limit history/lifecycle, listing index)
- two Redis Stream publishers
(`internal/adapters/redis/domainevents` for auxiliary domain events,
`internal/adapters/redis/lifecycleevents` for trusted user-lifecycle
events) sharing the same `*redis.Client`
- the trusted internal HTTP router
- the optional admin metrics listener
- the optional Redis-backed domain-event publishers
- service-local helpers for clock, IDs, and validation/policy adapters
Startup fails fast when Redis connectivity is unavailable or configuration is
invalid.
Startup fails fast when Redis or PostgreSQL connectivity is unavailable, the
mandatory connection-topology environment variables are missing, the
embedded migration sequence cannot be applied, or configuration is otherwise
invalid. The HTTP listeners do not open until every dependency check passes.
## Redis Namespaces
## Storage Backends
The service uses one Redis keyspace prefix plus one auxiliary domain-events
stream.
The service is split between two backends per
[`../../ARCHITECTURE.md §Persistence Backends`](../../ARCHITECTURE.md):
Configuration:
PostgreSQL holds source-of-truth durable state in the `user` schema:
- `USERSERVICE_REDIS_KEYSPACE_PREFIX`
- `USERSERVICE_REDIS_DOMAIN_EVENTS_STREAM`
- `USERSERVICE_REDIS_DOMAIN_EVENTS_STREAM_MAX_LEN`
- `accounts` (with `email` and `user_name` UNIQUE; `deleted_at` records the
Stage 22 soft-delete state)
- `blocked_emails` (one row per blocked address)
- `entitlement_records` plus the denormalised `entitlement_snapshots`
one-row-per-user current view
- `sanction_records` plus `sanction_active(user_id, sanction_code)`
- `limit_records` plus `limit_active(user_id, limit_code)`
The keyspace stores source-of-truth business state. The stream carries
post-commit auxiliary domain events and must not be treated as the source of
truth.
Indexes carry the listing surface (`accounts(created_at DESC, user_id
DESC)`), reverse-lookup filters (`accounts(declared_country)`,
`entitlement_snapshots(plan_code, is_paid)`,
`entitlement_snapshots(ends_at) WHERE is_paid AND ends_at IS NOT NULL`,
`sanction_active(sanction_code)`, `limit_active(limit_code)`), and the
per-user history scans.
Redis hosts only the two Stream publishers
(`USERSERVICE_REDIS_DOMAIN_EVENTS_STREAM`,
`USERSERVICE_REDIS_LIFECYCLE_EVENTS_STREAM`). It does not store any
durable user state after Stage 3 of `PG_PLAN.md`.
Decision records:
[`postgres-migration.md`](postgres-migration.md) for the schema and
storage decisions.
## Configuration Groups
Required for all process starts:
- `USERSERVICE_REDIS_ADDR`
- `USERSERVICE_REDIS_MASTER_ADDR`
- `USERSERVICE_REDIS_PASSWORD`
- `USERSERVICE_POSTGRES_PRIMARY_DSN`
Core process config:
@@ -116,16 +145,31 @@ Admin HTTP config:
- `USERSERVICE_ADMIN_HTTP_READ_TIMEOUT`
- `USERSERVICE_ADMIN_HTTP_IDLE_TIMEOUT`
Redis connectivity and namespace config:
Redis connectivity (consumed by `pkg/redisconn`):
- `USERSERVICE_REDIS_USERNAME`
- `USERSERVICE_REDIS_PASSWORD`
- `USERSERVICE_REDIS_REPLICA_ADDRS` (optional, comma-separated)
- `USERSERVICE_REDIS_DB`
- `USERSERVICE_REDIS_TLS_ENABLED`
- `USERSERVICE_REDIS_OPERATION_TIMEOUT`
- `USERSERVICE_REDIS_KEYSPACE_PREFIX`
Stream-shape (kept service-local):
- `USERSERVICE_REDIS_DOMAIN_EVENTS_STREAM`
- `USERSERVICE_REDIS_DOMAIN_EVENTS_STREAM_MAX_LEN`
- `USERSERVICE_REDIS_LIFECYCLE_EVENTS_STREAM`
- `USERSERVICE_REDIS_LIFECYCLE_EVENTS_STREAM_MAX_LEN`
PostgreSQL connectivity (consumed by `pkg/postgres`):
- `USERSERVICE_POSTGRES_REPLICA_DSNS` (optional, comma-separated)
- `USERSERVICE_POSTGRES_OPERATION_TIMEOUT`
- `USERSERVICE_POSTGRES_MAX_OPEN_CONNS`
- `USERSERVICE_POSTGRES_MAX_IDLE_CONNS`
- `USERSERVICE_POSTGRES_CONN_MAX_LIFETIME`
The retired Redis variables `USERSERVICE_REDIS_ADDR`,
`USERSERVICE_REDIS_USERNAME`, `USERSERVICE_REDIS_TLS_ENABLED`,
`USERSERVICE_REDIS_KEYSPACE_PREFIX` produce a startup error from
`pkg/redisconn` if set; unset them before starting the service.
Telemetry: