···11+use deadpool_postgres::{Config, Manager, ManagerConfig, Pool, RecyclingMethod};
22+use tokio_postgres::NoTls;
33+44+pub type DbPool = Pool;
55+66+/// Initialize database connection pool from environment
77+pub fn create_pool() -> Result<DbPool, Box<dyn std::error::Error>> {
88+ let db_url = std::env::var("DB_URL").map_err(|_| "DB_URL environment variable not set")?;
99+1010+ let config = db_url.parse::<tokio_postgres::Config>()?;
1111+1212+ let mut pool_config = Config::new();
1313+ pool_config.dbname = config.get_dbname().map(String::from);
1414+ pool_config.host = config.get_hosts().first().map(|h| match h {
1515+ tokio_postgres::config::Host::Tcp(s) => s.clone(),
1616+ #[cfg(unix)]
1717+ tokio_postgres::config::Host::Unix(p) => p.to_string_lossy().to_string(),
1818+ });
1919+ pool_config.port = config.get_ports().first().copied();
2020+ pool_config.user = config.get_user().map(String::from);
2121+ pool_config.password = config.get_password().map(|p| String::from_utf8_lossy(p).to_string());
2222+2323+ let mgr_config = ManagerConfig { recycling_method: RecyclingMethod::Fast };
2424+ let mgr = Manager::from_config(config, NoTls, mgr_config);
2525+2626+ Ok(Pool::builder(mgr).max_size(16).build()?)
2727+}
+9-1
crates/server/src/lib.rs
···11pub mod api;
22+pub mod db;
23pub mod middleware;
34pub mod state;
45···28292930 tracing::info!("Starting Malfestio Server...");
30313131- let state = state::AppState::new();
3232+ let pool = db::create_pool().map_err(|e| {
3333+ tracing::error!("Failed to create database pool: {}", e);
3434+ malfestio_core::Error::Database(format!("Failed to create database pool: {}", e))
3535+ })?;
3636+3737+ tracing::info!("Database connection pool created");
3838+3939+ let state = state::AppState::new(pool);
32403341 let auth_routes = Router::new()
3442 .route("/me", get(api::auth::me))
···11+-- Initial schema for Malfestio
22+-- This migration creates the core tables for decks, notes, and cards
33+-- Note: ATProto lexicon records go to PDS, this DB is for blob storage & private data
44+55+CREATE TABLE IF NOT EXISTS schema_migrations (
66+ id SERIAL PRIMARY KEY,
77+ version TEXT NOT NULL UNIQUE,
88+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
99+);
1010+1111+CREATE TABLE decks (
1212+ id UUID PRIMARY KEY,
1313+ owner_did TEXT NOT NULL,
1414+ title TEXT NOT NULL,
1515+ description TEXT NOT NULL,
1616+ tags TEXT[] NOT NULL DEFAULT '{}',
1717+ visibility JSONB NOT NULL, -- Stores { type: "Private" | "Unlisted" | "Public" | "SharedWith", content?: [...] }
1818+ published_at TIMESTAMPTZ,
1919+ fork_of UUID,
2020+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
2121+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
2222+);
2323+2424+CREATE INDEX idx_decks_owner_did ON decks(owner_did);
2525+CREATE INDEX idx_decks_visibility ON decks USING GIN(visibility);
2626+CREATE INDEX idx_decks_created_at ON decks(created_at DESC);
2727+2828+CREATE TABLE cards (
2929+ id UUID PRIMARY KEY,
3030+ owner_did TEXT NOT NULL,
3131+ deck_id UUID NOT NULL REFERENCES decks(id) ON DELETE CASCADE,
3232+ front TEXT NOT NULL,
3333+ back TEXT NOT NULL,
3434+ media_url TEXT,
3535+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3636+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
3737+);
3838+3939+CREATE INDEX idx_cards_deck_id ON cards(deck_id);
4040+CREATE INDEX idx_cards_owner_did ON cards(owner_did);
4141+4242+CREATE TABLE notes (
4343+ id UUID PRIMARY KEY,
4444+ owner_did TEXT NOT NULL,
4545+ title TEXT NOT NULL,
4646+ body TEXT NOT NULL,
4747+ tags TEXT[] NOT NULL DEFAULT '{}',
4848+ visibility JSONB NOT NULL,
4949+ published_at TIMESTAMPTZ,
5050+ links TEXT[] NOT NULL DEFAULT '{}', -- WikiLink style references to other notes
5151+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
5252+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
5353+);
5454+5555+CREATE INDEX idx_notes_owner_did ON notes(owner_did);
5656+CREATE INDEX idx_notes_visibility ON notes USING GIN(visibility);
5757+CREATE INDEX idx_notes_created_at ON notes(created_at DESC);
5858+CREATE INDEX idx_notes_links ON notes USING GIN(links);
5959+6060+CREATE OR REPLACE FUNCTION update_updated_at_column()
6161+RETURNS TRIGGER AS $$
6262+BEGIN
6363+ NEW.updated_at = NOW();
6464+ RETURN NEW;
6565+END;
6666+$$ LANGUAGE plpgsql;
6767+6868+CREATE TRIGGER update_decks_updated_at BEFORE UPDATE ON decks
6969+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
7070+7171+CREATE TRIGGER update_cards_updated_at BEFORE UPDATE ON cards
7272+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
7373+7474+CREATE TRIGGER update_notes_updated_at BEFORE UPDATE ON notes
7575+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();