11+++
22title = " Encrypted Relay"
33weight = 3
4- description = " How the sync relay works: store interface, auth, encryption, key exchange."
4+ description = " How the sync relay works: store interface, auth, encryption, key exchange, row-level security ."
55linkTitle = " Encrypted Relay"
66+++
77
@@ -136,6 +136,63 @@ Documents are synced as encrypted blobs, separate from the oplog:
136136- Oplog entries reference blobs via ` blob_ref ` field, which is
137137 stripped before applying to the local database
138138
139+ ## Row-level security
140+
141+ The relay enforces tenant isolation at the database level using
142+ [ Postgres row-level security] ( https://www.postgresql.org/docs/current/ddl-rowsecurity.html )
143+ (RLS). Even if application code has a bug, one household's data
144+ cannot leak to another.
145+
146+ ### How it works
147+
148+ RLS policies on ` ops ` and ` blobs ` restrict every query to rows
149+ matching the current session's ` app.household_id ` :
150+
151+ ``` sql
152+ CREATE POLICY ops_household_isolation ON ops
153+ USING (household_id = NULLIF(current_setting(' app.household_id' , true), ' ' ))
154+ WITH CHECK (household_id = NULLIF(current_setting(' app.household_id' , true), ' ' ));
155+ ```
156+
157+ ` FORCE ROW LEVEL SECURITY ` ensures policies apply even to the
158+ table owner, so no connection can bypass them.
159+
160+ ### rlsdb package
161+
162+ The ` rlsdb ` package (` internal/relay/rlsdb/ ` ) wraps the raw
163+ ` *gorm.DB ` in an unexported struct, making direct database access
164+ structurally impossible from outside the package. All queries go
165+ through one of two methods:
166+
167+ - ** ` Tx(ctx, householdID, fn) ` ** — opens a transaction, calls
168+ ` set_config('app.household_id', householdID, true) ` , then
169+ executes ` fn ` . This is the standard path for all household-scoped
170+ operations.
171+ - ** ` WithoutHousehold(ctx, fn) ` ** — opens a transaction with
172+ ` app.household_id ` cleared to empty string. RLS treats this as
173+ NULL, so no ` ops ` or ` blobs ` rows are visible. Reserved for
174+ methods that only touch non-RLS tables (` households ` , ` devices ` ,
175+ ` invites ` , ` key_exchanges ` ) where no household ID is available
176+ yet (e.g. device authentication, join flow).
177+
178+ Construction-time helpers:
179+ - ** ` Migrate(models...) ` ** — runs ` AutoMigrate ` with a dummy
180+ household ID so GORM's schema introspection works under `FORCE
181+ ROW LEVEL SECURITY`.
182+ - ** ` InitRLS(tables) ` ** — idempotently enables RLS and creates
183+ isolation policies for the given tables.
184+
185+ ### Protected tables
186+
187+ | Table | Scoping column | RLS enforced |
188+ | -------| ---------------| --------------|
189+ | ` ops ` | ` household_id ` | Yes |
190+ | ` blobs ` | ` household_id ` | Yes |
191+ | ` households ` | — | No (looked up by ID) |
192+ | ` devices ` | — | No (looked up by token hash) |
193+ | ` invites ` | — | No (looked up by code) |
194+ | ` key_exchanges ` | — | No (short-lived, scrubbed after use) |
195+
139196## Database schema (PostgreSQL)
140197
141198```
@@ -147,6 +204,9 @@ key_exchanges — id, household_id, joiner info, encrypted credentials
147204blobs — household_id, hash, data, size_bytes
148205```
149206
207+ RLS policies on ` ops ` and ` blobs ` enforce household isolation at the
208+ database level (see [ Row-level security] ( #row-level-security ) above).
209+
150210All table names use bare English (not ` pg_ ` prefixed). The Go
151211struct names use a ` pg ` prefix (` pgHousehold ` , ` pgDevice ` ) to
152212distinguish them from the ` sync.Household ` and ` sync.Device `
@@ -183,7 +243,8 @@ sequenceDiagram
183243 R->>R: assign sequence numbers, store ciphertext
184244 end
185245
186- Note over A,B: User runs `micasa pro invite`, shares code
246+ Note over A: User runs `micasa pro invite`
247+ Note over A,B: share invite code out-of-band
187248
188249 rect rgb(240, 235, 228)
189250 Note over A,B: Key exchange
@@ -233,6 +294,7 @@ internal/
233294 store.go Store interface (21 methods)
234295 memstore.go In-memory implementation (testing)
235296 pgstore.go PostgreSQL implementation (production)
297+ rlsdb/ RLS-aware DB wrapper (unexported *gorm.DB, Tx/WithoutHousehold)
236298 stripe.go Webhook signature verification
237299 tokencrypt.go AES-256-GCM token encryption at rest
238300 blob.go Blob storage constants and validation
0 commit comments