···11+-- Plugin registry
22+CREATE TABLE plugins (
33+ id TEXT PRIMARY KEY,
44+ source TEXT NOT NULL CHECK (source IN ('file', 'url')),
55+ url TEXT,
66+ sha256 TEXT,
77+ enabled BOOLEAN NOT NULL DEFAULT true,
88+ loaded_at TIMESTAMPTZ,
99+ api_version TEXT NOT NULL
1010+);
1111+1212+-- Plugin configuration
1313+CREATE TABLE plugin_configs (
1414+ plugin_id TEXT PRIMARY KEY REFERENCES plugins(id) ON DELETE CASCADE,
1515+ config JSONB NOT NULL DEFAULT '{}',
1616+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
1717+);
1818+1919+-- External account tokens (encrypted)
2020+CREATE TABLE external_account_tokens (
2121+ id TEXT PRIMARY KEY,
2222+ did TEXT NOT NULL,
2323+ plugin_id TEXT NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
2424+ account_id TEXT NOT NULL,
2525+ access_token BYTEA NOT NULL,
2626+ refresh_token BYTEA,
2727+ token_type TEXT,
2828+ scope TEXT,
2929+ expires_at TIMESTAMPTZ,
3030+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3131+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3232+ UNIQUE(did, plugin_id)
3333+);
3434+3535+-- Deduplication keys for sync records
3636+CREATE TABLE plugin_dedup_keys (
3737+ plugin_id TEXT NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
3838+ did TEXT NOT NULL,
3939+ dedup_key TEXT NOT NULL,
4040+ record_uri TEXT NOT NULL,
4141+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
4242+ PRIMARY KEY (plugin_id, did, dedup_key)
4343+);
4444+4545+-- KV storage for plugins (scoped per plugin + context)
4646+CREATE TABLE plugin_kv (
4747+ plugin_id TEXT NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
4848+ scope TEXT NOT NULL,
4949+ key TEXT NOT NULL,
5050+ value BYTEA NOT NULL,
5151+ expires_at TIMESTAMPTZ,
5252+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
5353+ PRIMARY KEY (plugin_id, scope, key)
5454+);
5555+5656+-- Index for KV expiration cleanup
5757+CREATE INDEX idx_plugin_kv_expires ON plugin_kv(expires_at) WHERE expires_at IS NOT NULL;
5858+5959+-- Index for token lookup by DID
6060+CREATE INDEX idx_external_tokens_did ON external_account_tokens(did);
···11+-- Plugin registry
22+CREATE TABLE plugins (
33+ id TEXT PRIMARY KEY,
44+ source TEXT NOT NULL CHECK (source IN ('file', 'url')),
55+ url TEXT,
66+ sha256 TEXT,
77+ enabled INTEGER NOT NULL DEFAULT 1,
88+ loaded_at TEXT,
99+ api_version TEXT NOT NULL
1010+);
1111+1212+-- Plugin configuration
1313+CREATE TABLE plugin_configs (
1414+ plugin_id TEXT PRIMARY KEY REFERENCES plugins(id) ON DELETE CASCADE,
1515+ config TEXT NOT NULL DEFAULT '{}',
1616+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1717+);
1818+1919+-- External account tokens (encrypted)
2020+CREATE TABLE external_account_tokens (
2121+ id TEXT PRIMARY KEY,
2222+ did TEXT NOT NULL,
2323+ plugin_id TEXT NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
2424+ account_id TEXT NOT NULL,
2525+ access_token BLOB NOT NULL,
2626+ refresh_token BLOB,
2727+ token_type TEXT,
2828+ scope TEXT,
2929+ expires_at TEXT,
3030+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3131+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
3232+ UNIQUE(did, plugin_id)
3333+);
3434+3535+-- Deduplication keys for sync records
3636+CREATE TABLE plugin_dedup_keys (
3737+ plugin_id TEXT NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
3838+ did TEXT NOT NULL,
3939+ dedup_key TEXT NOT NULL,
4040+ record_uri TEXT NOT NULL,
4141+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
4242+ PRIMARY KEY (plugin_id, did, dedup_key)
4343+);
4444+4545+-- KV storage for plugins (scoped per plugin + context)
4646+CREATE TABLE plugin_kv (
4747+ plugin_id TEXT NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
4848+ scope TEXT NOT NULL,
4949+ key TEXT NOT NULL,
5050+ value BLOB NOT NULL,
5151+ expires_at TEXT,
5252+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
5353+ PRIMARY KEY (plugin_id, scope, key)
5454+);
5555+5656+-- Index for KV expiration cleanup
5757+CREATE INDEX idx_plugin_kv_expires ON plugin_kv(expires_at) WHERE expires_at IS NOT NULL;
5858+5959+-- Index for token lookup by DID
6060+CREATE INDEX idx_external_tokens_did ON external_account_tokens(did);
···11+use crate::db::adapt_sql;
22+use crate::plugin::SyncRecord;
33+44+#[allow(dead_code)] // Used when full sync flow is implemented
55+#[derive(Debug, thiserror::Error)]
66+pub enum SyncError {
77+ #[error("Database error: {0}")]
88+ Database(#[from] sqlx::Error),
99+ #[error("Validation error: {0}")]
1010+ Validation(String),
1111+ #[error("PDS write error: {0}")]
1212+ PdsWrite(String),
1313+}
1414+1515+/// Process sync records from a plugin
1616+#[allow(dead_code)] // Used when full sync flow is implemented
1717+pub async fn process_sync_records(
1818+ db: &sqlx::AnyPool,
1919+ db_backend: crate::db::DatabaseBackend,
2020+ plugin_id: &str,
2121+ user_did: &str,
2222+ records: Vec<SyncRecord>,
2323+) -> Result<usize, SyncError> {
2424+ let mut processed = 0;
2525+2626+ for record in records {
2727+ // TODO: Validate against lexicon schema
2828+ // TODO: Check dedup_key
2929+ // TODO: Sign attestation
3030+ // TODO: Write to PDS
3131+3232+ // For now, just track dedup key
3333+ if let Some(dedup_key) = &record.dedup_key {
3434+ let sql = adapt_sql(
3535+ "INSERT INTO plugin_dedup_keys (plugin_id, did, dedup_key, record_uri, updated_at)
3636+ VALUES (?, ?, ?, ?, datetime('now'))
3737+ ON CONFLICT (plugin_id, did, dedup_key)
3838+ DO UPDATE SET record_uri = excluded.record_uri, updated_at = excluded.updated_at",
3939+ db_backend,
4040+ );
4141+4242+ sqlx::query(&sql)
4343+ .bind(plugin_id)
4444+ .bind(user_did)
4545+ .bind(dedup_key)
4646+ .bind("at://placeholder") // TODO: Real URI after PDS write
4747+ .execute(db)
4848+ .await?;
4949+ }
5050+5151+ processed += 1;
5252+ }
5353+5454+ Ok(processed)
5555+}
+3
src/lib.rs
···55pub mod dns;
66pub mod error;
77pub mod event_log;
88+pub mod external_auth;
89pub mod labeler;
910pub mod lexicon;
1011pub mod lua;
1212+pub mod plugin;
1113pub mod profile;
1214pub mod rate_limit;
1315pub mod record_refs;
···5658 pub rate_limiter: Arc<RateLimiter>,
5759 pub oauth: Arc<HappyViewOAuthClient>,
5860 pub cookie_key: axum_extra::extract::cookie::Key,
6161+ pub plugin_registry: Arc<plugin::PluginRegistry>,
5962}
60636164impl axum::extract::FromRef<AppState> for axum_extra::extract::cookie::Key {