Files
slopus--happy/docs/backend-architecture.md
wehub-resource-sync 98e40dac97
CLI Smoke Test / smoke-test-linux (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-linux (24) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (24) (push) Has been cancelled
Expo App TypeScript typecheck / typecheck (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:49 +08:00

400 lines
12 KiB
Markdown

# Backend Architecture
This document describes the Happy backend structure as implemented in `packages/happy-server`. It focuses on how the server is wired, how data flows through the system, and which subsystems handle which responsibilities.
## System overview
```mermaid
graph TB
subgraph Clients
CLI[CLI Client]
Mobile[Mobile App]
Daemon[Machine Daemon]
end
subgraph "Happy Server"
API[Fastify API]
Socket[Socket.IO]
Events[Event Router]
end
subgraph Storage
PG[(Postgres)]
Redis[(Redis)]
S3[(S3/MinIO)]
end
CLI --> API
Mobile --> API
Daemon --> API
CLI --> Socket
Mobile --> Socket
Daemon --> Socket
API --> PG
API --> S3
Socket --> Events
Events --> Redis
Events --> PG
```
## At a glance
- Runtime: Node.js + Fastify for HTTP, Socket.IO for realtime.
- Database: Postgres via Prisma.
- Cache/bus: Redis client is initialized (currently only pinged).
- Blob storage: S3-compatible (MinIO) for uploaded assets.
- Crypto: privacy-kit for auth tokens and encrypted service tokens.
- Metrics: Prometheus-style `/metrics` server + per-request HTTP metrics.
## Process lifecycle
Entry point: `packages/happy-server/sources/main.ts`.
```mermaid
flowchart TD
Start([main.ts]) --> DB[Connect Postgres]
DB --> Cache[Init Activity Cache]
Cache --> Redis[Redis ping]
Redis --> Crypto[Init Crypto Modules]
subgraph Crypto Initialization
Crypto --> Encrypt[initEncrypt - KeyTree]
Crypto --> GitHub[initGithub - OAuth/Webhooks]
Crypto --> S3[loadFiles - S3 Bucket]
Crypto --> Auth[auth.init - Token Gen]
end
Encrypt & GitHub & S3 & Auth --> Servers[Start Servers]
subgraph Server Startup
Servers --> API[API Server]
Servers --> Metrics[Metrics Server]
Servers --> DBMetrics[DB Metrics Updater]
Servers --> Presence[Presence Timeout Loop]
end
API & Metrics & DBMetrics & Presence --> Running([Running])
Running --> |SIGTERM| Shutdown[Shutdown Hooks]
Shutdown --> DBDisconnect[DB Disconnect]
Shutdown --> FlushCache[Flush Activity Cache]
```
Startup sequence:
1. Connect Postgres (`db.$connect()`).
2. Init activity cache (presence) and Redis connection check (`redis.ping()`).
3. Initialize crypto modules:
- `initEncrypt()` derives a KeyTree from `HANDY_MASTER_SECRET`.
- `initGithub()` configures GitHub App/webhooks if env vars exist.
- `loadFiles()` verifies S3 bucket access.
- `auth.init()` prepares token generator/verifier.
4. Start API server (`startApi()`), metrics server, database metrics updater, and presence timeout loop.
5. Remain alive until shutdown signal.
Shutdown hooks are registered for DB disconnect and activity-cache flush.
## API layer
`startApi()` in `sources/app/api/api.ts` wires the HTTP server:
- Fastify instance with Zod validators/serializers.
- Global hooks for monitoring and error handling.
- `authenticate` decorator that verifies Bearer tokens.
- Route modules under `sources/app/api/routes`.
- Socket.IO server attached at `/v1/updates`.
```mermaid
graph LR
subgraph "Fastify Server"
Hooks[Global Hooks]
Auth[authenticate decorator]
subgraph Routes
direction TB
R1[authRoutes]
R2[sessionRoutes]
R3[machinesRoutes]
R4[artifactsRoutes]
R5[accessKeysRoutes]
R6[kvRoutes]
R7[accountRoutes]
R8[userRoutes / feedRoutes]
R9[pushRoutes]
R10[connectRoutes / voiceRoutes]
end
end
SocketIO[Socket.IO /v1/updates]
Client --> Hooks --> Auth --> Routes
Client --> SocketIO
```
HTTP routes are organized by domain:
- Auth (`authRoutes`)
- Sessions + messages (`sessionRoutes`)
- Machines (`machinesRoutes`)
- Artifacts (`artifactsRoutes`)
- Access keys (`accessKeysRoutes`)
- Key-value store (`kvRoutes`)
- Account + usage (`accountRoutes`)
- Social + feed (`userRoutes`, `feedRoutes`)
- Push tokens (`pushRoutes`)
- Integrations (`connectRoutes`, `voiceRoutes`)
- Version checks (`versionRoutes`)
- Dev-only logging (`devRoutes`)
## Authentication and tokens
```mermaid
sequenceDiagram
participant Client
participant Server
participant DB as Postgres
participant Cache as Token Cache
Client->>Server: POST /v1/auth (signed challenge + public key)
Server->>DB: Upsert account by public key
DB-->>Server: Account record
Server->>Server: Generate Bearer token (privacy-kit)
Server->>Cache: Cache token
Server-->>Client: Bearer token
Note over Client,Cache: Subsequent requests
Client->>Server: Request + Bearer token
Server->>Cache: Verify token
Cache-->>Server: Valid / Account ID
Server-->>Client: Response
```
The backend does not store passwords. Instead:
- Clients authenticate with a signed challenge (`/v1/auth`) using a public key.
- The server upserts the account by public key and returns a Bearer token.
- Tokens are generated and verified by privacy-kit using `HANDY_MASTER_SECRET`.
- Tokens are cached in-memory for fast verification.
GitHub OAuth uses short-lived "ephemeral" tokens to protect the callback and is separate from normal auth.
## Realtime sync architecture
```mermaid
graph TB
subgraph Connections
U1[User Client 1]
U2[User Client 2]
S1[Session Client]
M1[Machine Daemon]
end
subgraph "Socket.IO Server"
Router[Event Router]
subgraph Scopes
US[user-scoped]
SS[session-scoped]
MS[machine-scoped]
end
end
U1 & U2 --> US
S1 --> SS
M1 --> MS
US & SS & MS --> Router
Router --> |persistent update| DB[(Postgres)]
Router --> |ephemeral event| Clients((Filtered Recipients))
```
### Connection types
Socket.IO connections are tagged by scope:
- `user-scoped`: receive all user updates.
- `session-scoped`: receive updates only for one session.
- `machine-scoped`: daemon connections for machine state.
### Event router
`EventRouter` (`sources/app/events/eventRouter.ts`) maintains per-user connection sets and routes:
- **Persistent `update` events**: database-backed changes with a user-level monotonic `seq`.
- **Ephemeral events**: presence/usage signals that are not persisted.
The router implements recipient filters so updates go only to interested connections (e.g., all session listeners or a specific machine).
### Update sequence numbers
- `Account.seq` is the per-user update counter. It is incremented by `allocateUserSeq` and used as `UpdatePayload.seq`.
- Sessions and artifacts maintain their own `seq` for per-object ordering.
## Presence and activity
```mermaid
flowchart LR
subgraph "High Frequency"
Events[session-alive / machine-alive]
Cache[Activity Cache]
end
subgraph "Batched Writes"
Batch[Batch Processor]
DB[(Postgres)]
end
subgraph "Timeout Loop"
Timer[10 min timer]
Offline[Mark Inactive]
Emit[Emit offline update]
end
Events --> |debounce| Cache
Cache --> |batch| Batch --> DB
Timer --> Cache
Cache --> |stale entries| Offline --> DB
Offline --> Emit
```
Presence is handled in `sources/app/presence`:
- `session-alive` and `machine-alive` events are debounced in memory (ActivityCache).
- Database writes are batched to reduce write load.
- A timeout loop marks sessions/machines inactive after 10 minutes of silence and emits an offline ephemeral update.
This splits high-frequency presence from durable storage updates.
## Storage and persistence
### Database (Prisma)
Prisma models live in `prisma/schema.prisma`. Key tables:
```mermaid
erDiagram
Account ||--o{ Session : owns
Account ||--o{ Machine : owns
Account ||--o{ Artifact : owns
Account ||--o{ UserKVStore : owns
Account ||--o{ UsageReport : tracks
Account ||--o{ UserRelationship : has
Account ||--o{ UserFeedItem : receives
Session ||--o{ SessionMessage : contains
Session ||--o{ AccessKey : grants
Machine ||--o{ AccessKey : receives
Account {
string publicKey
string profile
int seq
}
Session {
string metadata
int seq
}
Machine {
string metadata
string daemonState
}
Artifact {
string header
bytes body
string key
}
```
- `Account`: public key identity, profile, settings, seq counters.
- `Session` + `SessionMessage`: encrypted session metadata and message blobs.
- `Machine`: encrypted machine metadata + daemon state.
- `Artifact`: encrypted header/body + per-artifact key.
- `AccessKey`: encrypted per-session-per-machine access keys.
- `UserKVStore`: encrypted values with optimistic versions.
- `UsageReport`: usage aggregation per session/key.
- `UserRelationship` + `UserFeedItem`: social graph and feed.
### Transactions and retries
```mermaid
flowchart TD
Start([inTx call]) --> Begin[Begin Transaction]
Begin --> |Serializable| Exec[Execute Operations]
Exec --> Commit{Commit}
Commit --> |Success| After[afterTx callbacks]
After --> Emit[Emit Socket Updates]
Emit --> Done([Complete])
Commit --> |P2034 Error| Retry{Retry?}
Retry --> |Yes| Begin
Retry --> |Max retries| Fail([Throw Error])
```
`inTx()` wraps Prisma transactions with:
- Serializable isolation.
- Automatic retry on `P2034` (serialization failures).
- `afterTx()` to emit socket updates after commit.
This pattern is used for multi-write operations like batch KV mutation and session deletion.
### Blob storage (S3/MinIO)
The server uses S3-compatible storage for user assets (e.g., avatars):
- `storage/files.ts` configures the S3 client.
- `uploadImage` processes and stores files and writes metadata to `UploadedFile`.
- Public URLs are derived from `S3_PUBLIC_URL`.
### Redis
A Redis client is initialized in `main.ts` and pinged at startup. It can be expanded for caching or pub/sub if needed.
## Data confidentiality model
```mermaid
graph TB
subgraph "Client-side Encryption"
C1[Session metadata]
C2[Agent state]
C3[Daemon state]
C4[Message content]
C5[Artifacts]
C6[KV values]
end
subgraph "Server-side Encryption"
S1[GitHub OAuth tokens]
S2[OpenAI tokens]
S3[Anthropic tokens]
S4[Gemini tokens]
end
C1 & C2 & C3 & C4 & C5 & C6 --> |opaque blobs| DB[(Postgres)]
S1 & S2 & S3 & S4 --> |KeyTree from HANDY_MASTER_SECRET| DB
style C1 fill:#e1f5fe
style C2 fill:#e1f5fe
style C3 fill:#e1f5fe
style C4 fill:#e1f5fe
style C5 fill:#e1f5fe
style C6 fill:#e1f5fe
style S1 fill:#fff3e0
style S2 fill:#fff3e0
style S3 fill:#fff3e0
style S4 fill:#fff3e0
```
- Session metadata, agent state, daemon state, and message content are stored as opaque encrypted strings or blobs.
- Artifacts and KV values are stored encrypted and encoded as base64 on the wire.
- The server only encrypts/decrypts **service tokens** (GitHub OAuth tokens, vendor tokens) using the KeyTree derived from `HANDY_MASTER_SECRET`.
## Integrations
- **GitHub**: OAuth connect + webhook verification, optional if env vars are set.
- **AI vendors**: encrypted token storage for `openai`, `anthropic`, `gemini`.
- **Voice**: RevenueCat subscription check + ElevenLabs token minting.
- **Push tokens**: stored for later notification delivery.
## Observability
- `/health` route checks DB connectivity.
- Metrics server exposes `/metrics` for Prometheus.
- HTTP request counters and duration histograms are captured via Fastify hooks.
- WebSocket event counters and connection gauges are in `metrics2.ts`.
## Key implementation references
- Entrypoint: `packages/happy-server/sources/main.ts`
- API server: `packages/happy-server/sources/app/api/api.ts`
- Socket server: `packages/happy-server/sources/app/api/socket.ts`
- Event routing: `packages/happy-server/sources/app/events/eventRouter.ts`
- Presence: `packages/happy-server/sources/app/presence`
- Storage: `packages/happy-server/sources/storage`
- Prisma schema: `packages/happy-server/prisma/schema.prisma`