Architecture
Guide for contributors working on HappyView itself. For a user-facing overview, see the Introduction.
System overview
Queries go through the query handler to the database (SQLite by default, or Postgres). Writes go through the procedure handler to the user's PDS, then HappyView indexes the record locally. Real-time record events stream in via Jetstream; historical records are backfilled in-process by discovering repos via the relay's listReposByCollection and fetching records directly from each PDS. Labelers are external services that emit content labels over a direct WebSocket connection — they operate out-of-band, outside the relay/repo system.
Request flow
Reads (queries)
Writes (procedures)
Admin endpoints
Data flow
Real-time indexing
Backfill
Database schema
records
| Column | Type | Description |
|---|---|---|
uri | text (PK) | AT URI (at://did/collection/rkey) |
did | text | Author DID |
collection | text | Lexicon NSID |
rkey | text | Record key |
record | jsonb | Record value |
cid | text | Content identifier |
indexed_at | timestamptz | When HappyView indexed this record |
lexicons
| Column | Type | Description |
|---|---|---|
id | text (PK) | Lexicon NSID |
revision | integer | Incremented on upsert |
lexicon_json | jsonb | Raw lexicon definition |
lexicon_type | text | record, query, procedure, definitions |
backfill | boolean | Whether to backfill on upload |
target_collection | text | For queries/procedures: which record collection |
created_at | timestamptz | |
updated_at | timestamptz |
users
| Column | Type | Description |
|---|---|---|
id | uuid (PK) | |
did | text (unique) | User's atproto DID |
is_super | boolean | Whether this is the super user (only one allowed) |
created_at | timestamptz | |
last_used_at | timestamptz | Updated on each authenticated request |
user_permissions
| Column | Type | Description |
|---|---|---|
user_id | uuid (FK) | References users.id |
permission | text | Permission string (e.g. lexicons:create) |
| (PK) | Composite primary key: (user_id, permission) |
api_keys
| Column | Type | Description |
|---|---|---|
id | uuid (PK) | |
user_id | uuid (FK) | References users.id |
name | text | Descriptive label |
key_hash | text | SHA-256 hash of the full key |
key_prefix | text | First 11 characters for display |
permissions | text[] | Permissions granted to this key |
created_at | timestamptz | |
last_used_at | timestamptz | |
revoked_at | timestamptz | Set when revoked (soft delete) |
oauth_sessions
| Column | Type | Description |
|---|---|---|
did | text (PK) | User's atproto DID |
session_data | text | Serialized OAuth session (managed by atrium) |
created_at | timestamptz | |
updated_at | timestamptz |
oauth_state
| Column | Type | Description |
|---|---|---|
state_key | text (PK) | OAuth state parameter |
state_data | text | Serialized state (managed by atrium) |
created_at | timestamptz |
instance_settings
| Column | Type | Description |
|---|---|---|
key | text (PK) | Setting name (e.g. app_name) |
value | text | Setting value |
updated_at | timestamptz | Last modified |
event_logs
| Column | Type | Description |
|---|---|---|
id | uuid (PK) | |
event_type | text | Category.action format (e.g. user.created) |
severity | text | info, warn, or error |
actor_did | text | DID of the user who triggered the event |
subject | text | What was affected (DID, NSID, URI, etc.) |
detail | jsonb | Event-specific data |
created_at | timestamptz |
script_variables
| Column | Type | Description |
|---|---|---|
key | text (PK) | Variable name |
value | text | Variable value (encrypted at rest) |
created_at | timestamptz | |
updated_at | timestamptz |
backfill_jobs
| Column | Type | Description |
|---|---|---|
id | uuid (PK) | |
collection | text | Target collection (null = all) |
did | text | Target DID (null = all) |
status | text | pending, running, completed, failed |
total_repos | integer | |
processed_repos | integer | |
total_records | integer | |
error | text | Error message if failed |
started_at | timestamptz | |
completed_at | timestamptz | |
created_at | timestamptz |
Testing
# Unit tests (no database needed)
cargo test --lib
# All tests including end-to-end (SQLite by default)
cargo test
# Or run against Postgres
docker compose -f docker-compose.test.yml up -d
TEST_DATABASE_URL=postgres://happyview:happyview@localhost:5433/happyview_test cargo test
docker compose -f docker-compose.test.yml down
End-to-end tests use wiremock to mock external services (PLC directory, PDSes) and a real database for full integration coverage. By default tests use SQLite; set TEST_DATABASE_URL to a Postgres connection string to test against Postgres.