Database Schema
Overview
Spritz uses PostgreSQL with the pgvector extension for vector similarity search.
Core Tables
shout_users
User accounts and analytics.
CREATE TABLE shout_users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
wallet_address TEXT UNIQUE NOT NULL,
username TEXT UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
last_login TIMESTAMPTZ,
is_admin BOOLEAN DEFAULT FALSE,
beta_access BOOLEAN DEFAULT FALSE,
-- Moderation
is_banned BOOLEAN DEFAULT FALSE,
ban_reason TEXT,
-- Analytics
messages_sent INTEGER DEFAULT 0,
friends_count INTEGER DEFAULT 0,
voice_minutes NUMERIC DEFAULT 0,
video_minutes NUMERIC DEFAULT 0,
groups_joined INTEGER DEFAULT 0
);
CREATE INDEX idx_users_wallet ON shout_users(wallet_address);
CREATE INDEX idx_users_username ON shout_users(username);
shout_friends
Friend relationships.
CREATE TABLE shout_friends (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_address TEXT NOT NULL,
friend_address TEXT NOT NULL,
tag TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_address, friend_address)
);
CREATE INDEX idx_friends_user ON shout_friends(user_address);
CREATE INDEX idx_friends_friend ON shout_friends(friend_address);
shout_friend_requests
Friend request management.
CREATE TABLE shout_friend_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
from_address TEXT NOT NULL,
to_address TEXT NOT NULL,
status TEXT DEFAULT 'pending', -- 'pending', 'accepted', 'rejected'
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_requests_from ON shout_friend_requests(from_address);
CREATE INDEX idx_requests_to ON shout_friend_requests(to_address);
AI Agents Tables
shout_agents
Agent configurations.
CREATE TABLE shout_agents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_address TEXT NOT NULL,
name TEXT NOT NULL CHECK (char_length(name) <= 50),
personality TEXT CHECK (char_length(personality) <= 1000),
system_instructions TEXT,
model TEXT DEFAULT 'gemini-2.0-flash',
avatar_emoji TEXT DEFAULT '🤖',
avatar_url TEXT, -- Custom uploaded avatar image (optional)
-- Visibility: 'private', 'friends', 'public', 'official'
visibility TEXT DEFAULT 'private'
CHECK (visibility IN ('private', 'friends', 'public', 'official')),
-- Suggested questions (official agents, max 4)
suggested_questions TEXT[],
-- Capabilities
web_search_enabled BOOLEAN DEFAULT true,
use_knowledge_base BOOLEAN DEFAULT true,
-- Stats
message_count INTEGER DEFAULT 0,
-- Tags for discovery
tags JSONB DEFAULT '[]',
-- x402 configuration
x402_enabled BOOLEAN DEFAULT FALSE,
x402_price_cents INTEGER DEFAULT 1,
x402_network TEXT DEFAULT 'base-sepolia',
x402_wallet_address TEXT,
x402_pricing_mode TEXT DEFAULT 'global',
-- MCP & API tools
mcp_servers JSONB DEFAULT '[]',
api_tools JSONB DEFAULT '[]',
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
-- Constraints
CONSTRAINT unique_agent_name_per_user
UNIQUE (owner_address, name)
);
CREATE INDEX idx_agents_owner ON shout_agents(owner_address);
CREATE INDEX idx_agents_visibility ON shout_agents(visibility);
CREATE INDEX idx_agents_created ON shout_agents(created_at DESC);
CREATE INDEX idx_agents_tags ON shout_agents USING GIN(tags);
shout_agent_chats
Agent chat history.
CREATE TABLE shout_agent_chats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_id UUID NOT NULL REFERENCES shout_agents(id) ON DELETE CASCADE,
user_address TEXT NOT NULL,
session_id TEXT, -- For maintaining conversation context
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_agent_chats_agent ON shout_agent_chats(agent_id);
CREATE INDEX idx_agent_chats_user ON shout_agent_chats(user_address);
CREATE INDEX idx_agent_chats_session ON shout_agent_chats(session_id);
CREATE INDEX idx_agent_chats_created ON shout_agent_chats(created_at DESC);
shout_agent_knowledge
Knowledge base URLs with optional Firecrawl integration for advanced scraping.
CREATE TABLE shout_agent_knowledge (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_id UUID NOT NULL REFERENCES shout_agents(id) ON DELETE CASCADE,
-- Content info
title TEXT NOT NULL,
url TEXT NOT NULL,
content_type TEXT DEFAULT 'webpage', -- 'webpage', 'github', 'docs'
-- Processing status
status TEXT DEFAULT 'pending'
CHECK (status IN ('pending', 'processing', 'indexed', 'failed')),
error_message TEXT,
-- Embedding reference
embedding_id TEXT,
chunk_count INTEGER DEFAULT 0,
-- Firecrawl options (Official agents only)
scrape_method TEXT DEFAULT 'basic', -- 'basic' or 'firecrawl'
crawl_depth INTEGER DEFAULT 1, -- Max depth for Firecrawl crawls
exclude_patterns TEXT[], -- URL patterns to skip
-- Auto-sync configuration (Official agents only)
auto_sync BOOLEAN DEFAULT false,
sync_interval_hours INTEGER DEFAULT 24, -- Hours between syncs
last_synced_at TIMESTAMPTZ,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
indexed_at TIMESTAMPTZ,
-- Constraints
CONSTRAINT unique_url_per_agent UNIQUE (agent_id, url)
);
CREATE INDEX idx_agent_knowledge_agent ON shout_agent_knowledge(agent_id);
CREATE INDEX idx_agent_knowledge_status ON shout_agent_knowledge(status);
shout_agent_channel_memberships
Tracks which Official agents are present in which channels and location chats for @mention interactions.
CREATE TABLE shout_agent_channel_memberships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_id UUID NOT NULL REFERENCES shout_agents(id) ON DELETE CASCADE,
channel_type TEXT NOT NULL CHECK (channel_type IN ('global', 'channel', 'location')),
channel_id UUID REFERENCES shout_public_channels(id) ON DELETE CASCADE, -- NULL for global/location
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by TEXT, -- Admin who added the agent
UNIQUE(agent_id, channel_type, channel_id)
);
CREATE INDEX idx_agent_channel_memberships_channel
ON shout_agent_channel_memberships(channel_type, channel_id);
CREATE INDEX idx_agent_channel_memberships_agent
ON shout_agent_channel_memberships(agent_id);
-- RLS: Anyone can read, admins manage via API
ALTER TABLE shout_agent_channel_memberships ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Agent channel memberships are viewable by everyone"
ON shout_agent_channel_memberships FOR SELECT USING (true);
shout_knowledge_chunks
Vector embeddings for RAG.
-- Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE shout_knowledge_chunks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
agent_id UUID NOT NULL REFERENCES shout_agents(id) ON DELETE CASCADE,
knowledge_id UUID NOT NULL, -- References shout_agent_knowledge
content TEXT NOT NULL,
embedding vector(768), -- Google text-embedding-004
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- IVFFlat index for approximate nearest neighbor search
CREATE INDEX idx_knowledge_chunks_embedding
ON shout_knowledge_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
CREATE INDEX idx_knowledge_chunks_agent ON shout_knowledge_chunks(agent_id);
CREATE INDEX idx_knowledge_chunks_knowledge ON shout_knowledge_chunks(knowledge_id);
shout_agent_favorites
User favorites.
CREATE TABLE shout_agent_favorites (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_address TEXT NOT NULL,
agent_id UUID REFERENCES shout_agents(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_address, agent_id)
);
CREATE INDEX idx_favorites_user ON shout_agent_favorites(user_address);
CREATE INDEX idx_favorites_agent ON shout_agent_favorites(agent_id);
Group Chat Tables
shout_groups
Group configurations.
CREATE TABLE shout_groups (
id TEXT PRIMARY KEY, -- Group ID (uuid-like string)
name TEXT NOT NULL,
created_by TEXT NOT NULL, -- Creator's wallet address
symmetric_key TEXT NOT NULL, -- Encrypted symmetric key for the group
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_groups_created_by ON shout_groups(created_by);
shout_group_members
Group membership.
CREATE TABLE shout_group_members (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
group_id TEXT NOT NULL REFERENCES shout_groups(id) ON DELETE CASCADE,
member_address TEXT NOT NULL, -- Member's wallet address (lowercase)
joined_at TIMESTAMPTZ DEFAULT NOW(),
role TEXT DEFAULT 'member', -- 'admin' or 'member'
UNIQUE(group_id, member_address)
);
CREATE INDEX idx_group_members_group ON shout_group_members(group_id);
CREATE INDEX idx_group_members_member ON shout_group_members(member_address);
Chat Folders Tables
shout_chat_folders
User-defined folders for organizing chats (Telegram-style).
CREATE TABLE shout_chat_folders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_address TEXT NOT NULL,
emoji TEXT NOT NULL,
label TEXT NOT NULL DEFAULT '',
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_address, emoji)
);
CREATE INDEX idx_chat_folders_user ON shout_chat_folders(user_address);
shout_chat_folder_assignments
Maps chats to folders.
CREATE TABLE shout_chat_folder_assignments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_address TEXT NOT NULL,
chat_id TEXT NOT NULL,
chat_type TEXT NOT NULL CHECK (chat_type IN ('dm', 'group', 'channel', 'global')),
folder_emoji TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_address, chat_id)
);
CREATE INDEX idx_chat_folder_assignments_user ON shout_chat_folder_assignments(user_address);
CREATE INDEX idx_chat_folder_assignments_folder ON shout_chat_folder_assignments(user_address, folder_emoji);
Public Channel Tables
shout_public_channels
Public channel configurations.
CREATE TABLE shout_public_channels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
description TEXT,
emoji TEXT DEFAULT '💬',
category TEXT DEFAULT 'general',
slug TEXT UNIQUE, -- Custom URL slug (e.g., 'alien', 'ethereum')
creator_address TEXT,
is_official BOOLEAN DEFAULT false,
member_count INTEGER DEFAULT 0,
message_count INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_public_channels_category ON shout_public_channels(category);
shout_channel_members
Channel membership.
CREATE TABLE shout_channel_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL REFERENCES shout_public_channels(id) ON DELETE CASCADE,
user_address TEXT NOT NULL,
joined_at TIMESTAMPTZ DEFAULT NOW(),
notifications_muted BOOLEAN DEFAULT false,
last_read_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(channel_id, user_address)
);
CREATE INDEX idx_channel_members_user ON shout_channel_members(user_address);
CREATE INDEX idx_channel_members_channel ON shout_channel_members(channel_id);
shout_channel_messages
Public channel messages (unencrypted).
CREATE TABLE shout_channel_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL REFERENCES shout_public_channels(id) ON DELETE CASCADE,
sender_address TEXT NOT NULL,
content TEXT NOT NULL,
message_type TEXT DEFAULT 'text',
-- Pinned messages (admin feature)
is_pinned BOOLEAN DEFAULT false,
pinned_by TEXT,
pinned_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_channel_messages_channel ON shout_channel_messages(channel_id);
CREATE INDEX idx_channel_messages_created ON shout_channel_messages(created_at DESC);
CREATE INDEX idx_channel_messages_pinned ON shout_channel_messages(channel_id, is_pinned) WHERE is_pinned = true;
Location Chat Tables
shout_location_chats
Location-based chat rooms that allow users to connect with others nearby.
CREATE TABLE shout_location_chats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
latitude DOUBLE PRECISION NOT NULL,
longitude DOUBLE PRECISION NOT NULL,
radius_meters INTEGER DEFAULT 1000,
creator_address TEXT NOT NULL,
member_count INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_location_chats_coords ON shout_location_chats(latitude, longitude);
CREATE INDEX idx_location_chats_active ON shout_location_chats(is_active) WHERE is_active = true;
Moderation System Tables
shout_moderators
Moderators for global chat and specific channels.
CREATE TABLE shout_moderators (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_address TEXT NOT NULL,
channel_id UUID REFERENCES shout_public_channels(id) ON DELETE CASCADE, -- NULL = global chat
granted_by TEXT NOT NULL,
granted_at TIMESTAMPTZ DEFAULT NOW(),
-- Granular permissions
can_pin BOOLEAN DEFAULT true,
can_delete BOOLEAN DEFAULT true,
can_mute BOOLEAN DEFAULT true,
can_manage_mods BOOLEAN DEFAULT false,
notes TEXT,
UNIQUE(user_address, channel_id)
);
CREATE INDEX idx_moderators_user ON shout_moderators(user_address);
CREATE INDEX idx_moderators_channel ON shout_moderators(channel_id);
CREATE INDEX idx_moderators_global ON shout_moderators(channel_id) WHERE channel_id IS NULL;
shout_muted_users
Users muted from channels or global chat.
CREATE TABLE shout_muted_users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_address TEXT NOT NULL,
channel_id UUID REFERENCES shout_public_channels(id) ON DELETE CASCADE, -- NULL = global chat
muted_by TEXT NOT NULL,
muted_at TIMESTAMPTZ DEFAULT NOW(),
muted_until TIMESTAMPTZ, -- NULL = permanent mute
reason TEXT,
is_active BOOLEAN DEFAULT true,
unmuted_by TEXT,
unmuted_at TIMESTAMPTZ
);
CREATE INDEX idx_muted_users_address ON shout_muted_users(user_address);
CREATE INDEX idx_muted_users_channel ON shout_muted_users(channel_id);
CREATE INDEX idx_muted_users_active ON shout_muted_users(is_active, muted_until);
shout_moderation_log
Audit trail for all moderation actions.
CREATE TABLE shout_moderation_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
action_type TEXT NOT NULL, -- 'pin', 'unpin', 'delete', 'mute', 'unmute', 'promote_mod', 'demote_mod'
moderator_address TEXT NOT NULL,
target_user_address TEXT,
target_message_id UUID,
channel_id UUID REFERENCES shout_public_channels(id) ON DELETE SET NULL,
reason TEXT,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_mod_log_moderator ON shout_moderation_log(moderator_address);
CREATE INDEX idx_mod_log_target_user ON shout_moderation_log(target_user_address);
CREATE INDEX idx_mod_log_action ON shout_moderation_log(action_type);
CREATE INDEX idx_mod_log_created ON shout_moderation_log(created_at DESC);
shout_chat_rules
Room-level content and behavior rules.
CREATE TABLE shout_chat_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
chat_type TEXT NOT NULL CHECK (chat_type IN ('channel', 'alpha', 'location', 'group')),
chat_id TEXT,
links_allowed BOOLEAN DEFAULT true,
photos_allowed BOOLEAN DEFAULT true,
pixel_art_allowed BOOLEAN DEFAULT true,
gifs_allowed BOOLEAN DEFAULT true,
polls_allowed BOOLEAN DEFAULT true,
location_sharing_allowed BOOLEAN DEFAULT true,
voice_allowed BOOLEAN DEFAULT true,
slow_mode_seconds INTEGER DEFAULT 0,
read_only BOOLEAN DEFAULT false,
max_message_length INTEGER DEFAULT 0,
rules_text TEXT,
updated_by TEXT,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(chat_type, chat_id)
);
CREATE INDEX idx_chat_rules_lookup ON shout_chat_rules(chat_type, chat_id);
shout_room_bans
Per-room user bans with optional expiration.
CREATE TABLE shout_room_bans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
chat_type TEXT NOT NULL CHECK (chat_type IN ('channel', 'alpha', 'location', 'group')),
chat_id TEXT,
user_address TEXT NOT NULL,
banned_by TEXT NOT NULL,
reason TEXT,
banned_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
banned_until TIMESTAMP WITH TIME ZONE, -- NULL = permanent
is_active BOOLEAN DEFAULT true
);
CREATE UNIQUE INDEX idx_room_bans_unique
ON shout_room_bans(chat_type, COALESCE(chat_id, '__global__'), user_address)
WHERE is_active = true;
CREATE INDEX idx_room_bans_lookup
ON shout_room_bans(chat_type, chat_id, user_address, is_active);
CREATE INDEX idx_room_bans_user ON shout_room_bans(user_address, is_active);
See Room Rules & Moderation for full API documentation.
shout_blocked_words
Global or per-room blocked words/phrases for anti-scam and content filtering.
CREATE TABLE shout_blocked_words (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
word TEXT NOT NULL,
scope TEXT NOT NULL DEFAULT 'global' CHECK (scope IN ('global', 'room')),
chat_type TEXT CHECK (chat_type IN ('channel', 'alpha', 'location', 'group')),
chat_id TEXT,
action TEXT NOT NULL DEFAULT 'block' CHECK (action IN ('block', 'flag', 'mute')),
is_regex BOOLEAN DEFAULT false,
added_by TEXT NOT NULL,
added_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_active BOOLEAN DEFAULT true
);
CREATE UNIQUE INDEX idx_blocked_words_unique
ON shout_blocked_words(LOWER(word), scope, COALESCE(chat_type, '__none__'), COALESCE(chat_id, '__global__'))
WHERE is_active = true;
Message Soft Delete
Messages support soft deletion for moderation across all chat types:
-- DM messages: soft delete (content replaced with "[Message deleted]")
ALTER TABLE shout_messages
ADD COLUMN is_deleted BOOLEAN DEFAULT false;
-- Alpha chat messages
ALTER TABLE shout_alpha_messages
ADD COLUMN is_deleted BOOLEAN DEFAULT false,
ADD COLUMN deleted_by TEXT,
ADD COLUMN deleted_at TIMESTAMPTZ,
ADD COLUMN delete_reason TEXT;
-- Channel messages
ALTER TABLE shout_channel_messages
ADD COLUMN is_deleted BOOLEAN DEFAULT false,
ADD COLUMN deleted_by TEXT,
ADD COLUMN deleted_at TIMESTAMPTZ,
ADD COLUMN delete_reason TEXT;
Location chat messages use hard deletes (the row is removed from shout_location_chat_messages entirely) rather than soft deletes.
shout_admin_activity
Audit log for admin actions (bans, unbans, moderation).
CREATE TABLE shout_admin_activity (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
admin_address TEXT NOT NULL,
action TEXT NOT NULL, -- 'ban_user', 'unban_user', etc.
target_address TEXT,
details JSONB, -- { reason: "..." }
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_admin_activity_admin ON shout_admin_activity(admin_address);
CREATE INDEX idx_admin_activity_target ON shout_admin_activity(target_address);
Calendar & Scheduling Tables
shout_calendar_connections
OAuth connections to calendar providers.
CREATE TABLE shout_calendar_connections (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
wallet_address TEXT NOT NULL,
provider TEXT NOT NULL DEFAULT 'google',
access_token TEXT NOT NULL,
refresh_token TEXT,
token_expires_at TIMESTAMPTZ,
calendar_id TEXT,
calendar_email TEXT,
is_active BOOLEAN DEFAULT true,
last_sync_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(wallet_address, provider)
);
CREATE INDEX idx_calendar_connections_wallet ON shout_calendar_connections(wallet_address);
shout_availability_windows
User-defined availability windows for scheduling.
CREATE TABLE shout_availability_windows (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
wallet_address TEXT NOT NULL,
name TEXT NOT NULL,
day_of_week INTEGER NOT NULL CHECK (day_of_week >= 0 AND day_of_week <= 6),
start_time TIME NOT NULL,
end_time TIME NOT NULL,
timezone TEXT NOT NULL DEFAULT 'UTC',
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_availability_windows_wallet ON shout_availability_windows(wallet_address);
CREATE INDEX idx_availability_windows_day ON shout_availability_windows(day_of_week);
shout_scheduled_calls
Scheduled calls/meetings between users.
CREATE TABLE shout_scheduled_calls (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
scheduler_wallet_address TEXT NOT NULL,
recipient_wallet_address TEXT NOT NULL,
scheduled_at TIMESTAMPTZ NOT NULL,
duration_minutes INTEGER DEFAULT 30,
title TEXT,
status TEXT NOT NULL DEFAULT 'pending',
payment_required BOOLEAN DEFAULT false,
payment_amount TEXT,
payment_token TEXT,
payment_status TEXT DEFAULT 'pending',
calendar_event_id TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_scheduled_calls_scheduler ON shout_scheduled_calls(scheduler_wallet_address);
CREATE INDEX idx_scheduled_calls_recipient ON shout_scheduled_calls(recipient_wallet_address);
CREATE INDEX idx_scheduled_calls_scheduled_at ON shout_scheduled_calls(scheduled_at);
Call History
shout_call_history
Voice and video call history.
CREATE TABLE shout_call_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
caller_address TEXT NOT NULL,
callee_address TEXT NOT NULL,
call_type TEXT NOT NULL CHECK (call_type IN ('audio', 'video')),
status TEXT NOT NULL CHECK (status IN ('completed', 'missed', 'declined', 'failed')),
started_at TIMESTAMPTZ DEFAULT NOW(),
ended_at TIMESTAMPTZ,
duration_seconds INTEGER DEFAULT 0,
channel_name TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_call_history_caller ON shout_call_history(caller_address);
CREATE INDEX idx_call_history_callee ON shout_call_history(callee_address);
CREATE INDEX idx_call_history_created ON shout_call_history(created_at DESC);
Profile Widgets Tables
profile_widgets
Customizable Bento-style profile widgets.
CREATE TABLE profile_widgets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_address TEXT NOT NULL,
widget_type TEXT NOT NULL, -- 'map', 'image', 'text', 'social_embed', 'nft', 'link', 'spotify', 'github', 'video', 'countdown', 'stats'
size TEXT NOT NULL DEFAULT '1x1', -- '1x1', '2x1', '1x2', '2x2', '4x1', '4x2'
position INTEGER NOT NULL DEFAULT 0,
config JSONB NOT NULL DEFAULT '{}',
is_visible BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_profile_widgets_user ON profile_widgets(user_address);
CREATE INDEX idx_profile_widgets_position ON profile_widgets(user_address, position);
profile_themes
User profile theme and styling preferences.
CREATE TABLE profile_themes (
user_address TEXT PRIMARY KEY,
background_type TEXT NOT NULL DEFAULT 'solid', -- 'solid', 'gradient', 'image', 'mesh'
background_value TEXT NOT NULL DEFAULT '#09090b',
accent_color TEXT NOT NULL DEFAULT '#f97316',
secondary_color TEXT,
text_color TEXT NOT NULL DEFAULT '#ffffff',
card_style TEXT NOT NULL DEFAULT 'rounded', -- 'rounded', 'sharp', 'pill'
card_background TEXT NOT NULL DEFAULT 'rgba(24, 24, 27, 0.8)',
card_border TEXT DEFAULT 'rgba(63, 63, 70, 0.5)',
font_family TEXT NOT NULL DEFAULT 'system', -- 'system', 'inter', 'mono', 'serif'
show_spritz_badge BOOLEAN NOT NULL DEFAULT true,
custom_css TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Custom Avatar Columns
Additional columns on shout_user_settings for custom avatars:
ALTER TABLE shout_user_settings
ADD COLUMN custom_avatar_url TEXT,
ADD COLUMN use_custom_avatar BOOLEAN DEFAULT FALSE;
Username Tables
shout_usernames
Username claims and management.
CREATE TABLE shout_usernames (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
wallet_address TEXT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_usernames_wallet ON shout_usernames(wallet_address);
CREATE INDEX idx_usernames_username ON shout_usernames(username);
Constraints:
- Username must be 3-20 characters
- Only lowercase letters, numbers, and underscores allowed
- Reserved usernames are blocked (see Security)
Authentication Tables
passkey_credentials
WebAuthn/passkey credential storage.
CREATE TABLE passkey_credentials (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
credential_id TEXT NOT NULL UNIQUE,
public_key TEXT NOT NULL,
counter BIGINT NOT NULL DEFAULT 0,
user_address TEXT NOT NULL,
display_name TEXT,
aaguid TEXT,
transports TEXT[],
backed_up BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW(),
last_used_at TIMESTAMPTZ,
device_info JSONB
);
CREATE INDEX idx_passkey_credentials_user_address ON passkey_credentials(user_address);
CREATE INDEX idx_passkey_credentials_credential_id ON passkey_credentials(credential_id);
passkey_challenges
Active WebAuthn challenges (short-lived).
CREATE TABLE passkey_challenges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
challenge TEXT NOT NULL UNIQUE,
ceremony_type TEXT NOT NULL CHECK (ceremony_type IN ('registration', 'authentication')),
user_address TEXT,
expires_at TIMESTAMPTZ NOT NULL,
used BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_passkey_challenges_challenge ON passkey_challenges(challenge);
Bug Reports
shout_bug_reports
User-submitted bug reports.
CREATE TABLE shout_bug_reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_address TEXT NOT NULL,
category TEXT NOT NULL CHECK (category IN ('Agents', 'Friends', 'Calls', 'Chats', 'Rooms', 'Livestream', 'Settings', 'Configuration', 'Other')),
description TEXT NOT NULL,
replication_steps TEXT,
status TEXT DEFAULT 'open' CHECK (status IN ('open', 'in_progress', 'resolved', 'closed')),
admin_notes TEXT,
resolved_by TEXT,
resolved_at TIMESTAMPTZ,
github_issue_url TEXT,
github_issue_number INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_bug_reports_user_address ON shout_bug_reports(user_address);
CREATE INDEX idx_bug_reports_category ON shout_bug_reports(category);
CREATE INDEX idx_bug_reports_status ON shout_bug_reports(status);
Wallet Analytics Tables
shout_wallet_transactions
Tracks all wallet transactions for analytics.
CREATE TABLE shout_wallet_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_address VARCHAR(66) NOT NULL,
smart_wallet_address VARCHAR(42) NOT NULL,
tx_hash VARCHAR(66) NOT NULL,
chain_id INTEGER NOT NULL,
chain_name VARCHAR(50) NOT NULL,
from_address VARCHAR(42) NOT NULL,
to_address VARCHAR(42) NOT NULL,
token_symbol VARCHAR(20) NOT NULL,
token_address VARCHAR(42),
amount VARCHAR(78) NOT NULL,
amount_formatted DECIMAL(38, 18),
amount_usd DECIMAL(18, 2),
tx_type VARCHAR(20) NOT NULL DEFAULT 'send',
status VARCHAR(20) NOT NULL DEFAULT 'pending',
gas_used VARCHAR(78),
gas_price VARCHAR(78),
created_at TIMESTAMPTZ DEFAULT NOW(),
confirmed_at TIMESTAMPTZ,
CONSTRAINT unique_tx_hash_chain UNIQUE(tx_hash, chain_id)
);
CREATE INDEX idx_wallet_tx_user ON shout_wallet_transactions(user_address);
CREATE INDEX idx_wallet_tx_smart_wallet ON shout_wallet_transactions(smart_wallet_address);
CREATE INDEX idx_wallet_tx_chain ON shout_wallet_transactions(chain_id);
CREATE INDEX idx_wallet_tx_created ON shout_wallet_transactions(created_at);
shout_wallet_network_stats
Aggregated network usage statistics.
CREATE TABLE shout_wallet_network_stats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
chain_id INTEGER NOT NULL UNIQUE,
chain_name VARCHAR(50) NOT NULL,
total_transactions INTEGER DEFAULT 0,
total_volume_usd DECIMAL(24, 2) DEFAULT 0,
unique_users INTEGER DEFAULT 0,
last_updated_at TIMESTAMPTZ DEFAULT NOW()
);
User Wallet Analytics Columns
Additional columns on shout_users for wallet analytics:
ALTER TABLE shout_users
ADD COLUMN wallet_tx_count INTEGER DEFAULT 0,
ADD COLUMN wallet_volume_usd DECIMAL(24, 2) DEFAULT 0,
ADD COLUMN last_wallet_tx_at TIMESTAMPTZ,
ADD COLUMN preferred_chain_id INTEGER;
Streaming Tables
shout_streams
Livestreaming sessions.
CREATE TABLE shout_streams (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_address TEXT NOT NULL,
stream_id TEXT NOT NULL, -- Livepeer stream ID
stream_key TEXT, -- Livepeer stream key (for WHIP)
playback_id TEXT, -- Livepeer playback ID
title TEXT,
description TEXT,
status TEXT DEFAULT 'idle' CHECK (status IN ('idle', 'live', 'ended')),
viewer_count INTEGER DEFAULT 0,
started_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_streams_user ON shout_streams(user_address);
CREATE INDEX idx_streams_status ON shout_streams(status);
CREATE INDEX idx_streams_created ON shout_streams(created_at DESC);
shout_stream_assets
Stream recordings.
CREATE TABLE shout_stream_assets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
stream_id UUID REFERENCES shout_streams(id) ON DELETE CASCADE,
user_address TEXT NOT NULL,
asset_id TEXT NOT NULL UNIQUE, -- Livepeer asset ID
playback_id TEXT,
playback_url TEXT,
download_url TEXT,
duration_seconds NUMERIC,
size_bytes BIGINT,
status TEXT DEFAULT 'processing'
CHECK (status IN ('processing', 'ready', 'failed')),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_stream_assets_stream ON shout_stream_assets(stream_id);
CREATE INDEX idx_stream_assets_status ON shout_stream_assets(status);
shout_stream_viewers
Active viewer tracking.
CREATE TABLE shout_stream_viewers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
stream_id UUID REFERENCES shout_streams(id) ON DELETE CASCADE,
user_address TEXT NOT NULL,
joined_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(stream_id, user_address)
);
CREATE INDEX idx_stream_viewers_stream ON shout_stream_viewers(stream_id);
CREATE INDEX idx_stream_viewers_user ON shout_stream_viewers(user_address);
Functions
Vector Similarity Search
CREATE OR REPLACE FUNCTION match_knowledge_chunks(
p_agent_id UUID,
p_query_embedding vector(768),
p_match_count INT DEFAULT 5,
p_match_threshold FLOAT DEFAULT 0.3
)
RETURNS TABLE (
id UUID,
content TEXT,
similarity FLOAT,
metadata JSONB
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
kc.id,
kc.content,
1 - (kc.embedding <=> p_query_embedding) AS similarity,
kc.metadata
FROM shout_knowledge_chunks kc
WHERE
kc.agent_id = p_agent_id
AND 1 - (kc.embedding <=> p_query_embedding) > p_match_threshold
ORDER BY kc.embedding <=> p_query_embedding
LIMIT p_match_count;
END;
$$;
Increment Agent Messages
CREATE OR REPLACE FUNCTION increment_agent_messages(p_agent_id UUID)
RETURNS void AS $$
BEGIN
UPDATE shout_agents
SET message_count = message_count + 1,
updated_at = NOW()
WHERE id = p_agent_id;
END;
$$ LANGUAGE plpgsql;
Check If User Is Muted
CREATE OR REPLACE FUNCTION is_user_muted(
p_user_address TEXT,
p_channel_id UUID DEFAULT NULL
)
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1 FROM shout_muted_users
WHERE user_address = LOWER(p_user_address)
AND (channel_id = p_channel_id OR (p_channel_id IS NULL AND channel_id IS NULL))
AND is_active = true
AND (muted_until IS NULL OR muted_until > NOW())
);
END;
$$ LANGUAGE plpgsql;
Check If User Is Moderator
CREATE OR REPLACE FUNCTION is_user_moderator(
p_user_address TEXT,
p_channel_id UUID DEFAULT NULL
)
RETURNS BOOLEAN AS $$
BEGIN
-- Check if global admin first
IF EXISTS (SELECT 1 FROM shout_admins WHERE wallet_address = LOWER(p_user_address)) THEN
RETURN true;
END IF;
-- Check moderator table
RETURN EXISTS (
SELECT 1 FROM shout_moderators
WHERE user_address = LOWER(p_user_address)
AND (channel_id = p_channel_id OR (p_channel_id IS NULL AND channel_id IS NULL))
);
END;
$$ LANGUAGE plpgsql;
Row Level Security (RLS)
Enable RLS
ALTER TABLE shout_agents ENABLE ROW LEVEL SECURITY;
ALTER TABLE shout_agent_chats ENABLE ROW LEVEL SECURITY;
ALTER TABLE shout_agent_knowledge ENABLE ROW LEVEL SECURITY;
RLS Policies
-- Users can manage own agents
CREATE POLICY "Users can manage own agents" ON shout_agents
FOR ALL USING (true); -- Simplified for now
-- Users can access agent chats
CREATE POLICY "Users can access agent chats" ON shout_agent_chats
FOR ALL USING (true); -- Simplified for now
Realtime Subscriptions
-- Enable realtime for key tables
ALTER PUBLICATION spritz_realtime ADD TABLE shout_agents;
ALTER PUBLICATION spritz_realtime ADD TABLE shout_streams;
ALTER PUBLICATION spritz_realtime ADD TABLE shout_groups;
ALTER PUBLICATION spritz_realtime ADD TABLE shout_group_members;
ALTER PUBLICATION spritz_realtime ADD TABLE shout_channel_messages;
ALTER PUBLICATION spritz_realtime ADD TABLE shout_moderators;
ALTER PUBLICATION spritz_realtime ADD TABLE shout_muted_users;
User Moderation Tables
shout_muted_conversations
Stores mute settings for conversations (DMs, groups, channels).
CREATE TABLE shout_muted_conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_address TEXT NOT NULL,
conversation_type TEXT NOT NULL CHECK (conversation_type IN ('dm', 'group', 'channel')),
conversation_id TEXT NOT NULL, -- Peer address for DMs, group/channel ID for others
muted_until TIMESTAMPTZ, -- NULL = forever
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_address, conversation_type, conversation_id)
);
CREATE INDEX idx_muted_conversations_user ON shout_muted_conversations(user_address);
CREATE INDEX idx_muted_conversations_until ON shout_muted_conversations(muted_until);
shout_blocked_users
Stores user blocks (bidirectional - blocked user can't message blocker).
CREATE TABLE shout_blocked_users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
blocker_address TEXT NOT NULL,
blocked_address TEXT NOT NULL,
reason TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(blocker_address, blocked_address)
);
CREATE INDEX idx_blocked_users_blocker ON shout_blocked_users(blocker_address);
CREATE INDEX idx_blocked_users_blocked ON shout_blocked_users(blocked_address);
shout_user_reports
Stores reports for admin review.
CREATE TABLE shout_user_reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
reporter_address TEXT NOT NULL,
reported_address TEXT NOT NULL,
report_type TEXT NOT NULL CHECK (report_type IN (
'spam', 'harassment', 'hate_speech', 'violence',
'scam', 'impersonation', 'inappropriate_content', 'other'
)),
description TEXT,
conversation_type TEXT, -- Context: dm, group, channel
conversation_id TEXT,
message_id TEXT, -- Specific message being reported
message_content TEXT, -- Snapshot of content (max 1000 chars)
status TEXT DEFAULT 'pending' CHECK (status IN (
'pending', 'reviewed', 'action_taken', 'dismissed'
)),
admin_notes TEXT,
reviewed_at TIMESTAMPTZ,
reviewed_by TEXT, -- Admin who reviewed
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_reports_reporter ON shout_user_reports(reporter_address);
CREATE INDEX idx_reports_reported ON shout_user_reports(reported_address);
CREATE INDEX idx_reports_status ON shout_user_reports(status);
CREATE INDEX idx_reports_type ON shout_user_reports(report_type);
CREATE INDEX idx_reports_created ON shout_user_reports(created_at DESC);
Moderation RLS Policies
ALTER TABLE shout_muted_conversations ENABLE ROW LEVEL SECURITY;
ALTER TABLE shout_blocked_users ENABLE ROW LEVEL SECURITY;
ALTER TABLE shout_user_reports ENABLE ROW LEVEL SECURITY;
-- Users can manage their own mutes
CREATE POLICY "Users can view/manage own mutes" ON shout_muted_conversations FOR ALL USING (true);
-- Users can view blocks (need to check if blocked by others)
CREATE POLICY "Users can view blocks" ON shout_blocked_users FOR SELECT USING (true);
CREATE POLICY "Users can create/delete own blocks" ON shout_blocked_users FOR ALL USING (true);
-- Users can create reports, admins can view all
CREATE POLICY "Users can create reports" ON shout_user_reports FOR INSERT WITH CHECK (true);
CREATE POLICY "Users/admins can view reports" ON shout_user_reports FOR SELECT USING (true);
CREATE POLICY "Admins can update reports" ON shout_user_reports FOR UPDATE USING (true);
Migration Scripts
All migration scripts are located in /migrations directory:
Core Tables
agents.sql- Agent configurations and chat historyagents_x402.sql- x402 payment fieldsagents_mcp.sql- MCP server configurationagents_tags.sql- Tags for discoveryagents_api_tools.sql- Custom API toolsembeddings.sql- Vector search setup (pgvector)favorite_agents.sql- Agent favorites044_agent_avatar.sql- Custom agent avatar uploads
Profile
042_profile_widgets.sql- Bento-style profile widgets043_custom_avatar.sql- Custom profile avatar uploads
Communication
group_chats.sql- Group chat tablespublic_channels.sql- Public channels systemchat_enhancements.sql- Typing status, read receipts, reactionschannel_chat_enhancements.sql- Channel reactionspinned_messages.sql- Admin pinned messages in channels043_chat_folders.sql- Chat folder organization
Streaming & Calls
call_history.sql- Voice/video call historypermanent_rooms.sql- Permanent meeting roomsinstant_rooms.sql- Instant meeting rooms
Authentication
passkey_credentials.sql- WebAuthn/passkey storageemail_login.sql- Email login verification
Scheduling
google_calendar.sql- Calendar connections and availabilityscheduling_links.sql- Shareable scheduling linksscheduling_settings.sql- User scheduling preferences
User Management
user_analytics.sql- User activity trackingfriend_tags.sql- Friend organization tagsbug_reports.sql- Bug report systemadmin_system.sql- Admin functionality
Wallet & Analytics
042_wallet_analytics.sql- Wallet transaction tracking
Moderation
041_moderation_system.sql- Moderators, muted users, and audit log
Usernames
045_usernames.sql- Username claims and reserved name validation
See the repository's /migrations folder for complete migration scripts.