···11+-- V003: Relay-level signing keys
22+-- Applied in a single transaction by the migration runner.
33+--
44+-- Relay signing keys are operator-level keys used to sign user repo commits.
55+-- Unlike signing_keys (V002), these are not tied to a specific account DID.
66+77+-- ── Relay Signing Keys ───────────────────────────────────────────────────────
88+99+-- WITHOUT ROWID: keys are always fetched by their did:key URI (the primary key).
1010+CREATE TABLE relay_signing_keys (
1111+ id TEXT NOT NULL, -- full did:key:z... URI; derived from public key
1212+ algorithm TEXT NOT NULL, -- "p256"
1313+ public_key TEXT NOT NULL, -- multibase base58btc compressed point
1414+ private_key_encrypted TEXT NOT NULL, -- base64(12-byte nonce || 32-byte ciphertext || 16-byte tag)
1515+ created_at TEXT NOT NULL, -- ISO 8601 UTC
1616+ PRIMARY KEY (id)
1717+) WITHOUT ROWID;
+26-3
crates/relay/src/db/mod.rs
···3636 version: 2,
3737 sql: include_str!("migrations/V002__auth_identity.sql"),
3838 },
3939+ Migration {
4040+ version: 3,
4141+ sql: include_str!("migrations/V003__relay_signing_keys.sql"),
4242+ },
3943];
40444145/// Open a WAL-mode SQLite connection pool with a maximum of 1 connection.
···324328 }
325329 }
326330327327- /// schema_migrations must contain exactly 2 rows after applying V001 + V002.
331331+ /// schema_migrations must contain one row per migration in MIGRATIONS.
328332 #[tokio::test]
329329- async fn v002_migration_count_is_two_after_both_migrations() {
333333+ async fn all_migrations_recorded_in_schema_migrations() {
330334 let pool = in_memory_pool().await;
331335 run_migrations(&pool).await.unwrap();
332336···334338 .fetch_one(&pool)
335339 .await
336340 .unwrap();
337337- assert_eq!(count, 2, "both V001 and V002 must be recorded");
341341+ assert_eq!(
342342+ count,
343343+ MIGRATIONS.len() as i64,
344344+ "schema_migrations must have one row per migration in MIGRATIONS"
345345+ );
338346 }
339347340348 /// Running migrations twice must not drop or recreate V002 tables.
···724732 detail.contains("idx_sessions_did"),
725733 "sessions WHERE did query must use idx_sessions_did; got: {detail}"
726734 );
735735+ }
736736+737737+ // ── V003 tests ───────────────────────────────────────────────────────────
738738+739739+ #[tokio::test]
740740+ async fn v003_relay_signing_keys_table_exists() {
741741+ let pool = in_memory_pool().await;
742742+ run_migrations(&pool).await.unwrap();
743743+744744+ // Verify the table exists by performing a SELECT with no rows expected.
745745+ let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM relay_signing_keys")
746746+ .fetch_one(&pool)
747747+ .await
748748+ .expect("relay_signing_keys table must exist after V003 migration");
749749+ assert_eq!(count.0, 0, "table must be empty after migration");
727750 }
728751729752 /// WAL mode requires a real file — use tempfile here, not :memory:.