A community based topic aggregation platform built on atproto
1-- +goose Up
2-- Create aggregators tables for indexing aggregator service declarations and authorizations
3-- These records are indexed from Jetstream firehose consumer
4
5-- ============================================================================
6-- Table: aggregators
7-- Purpose: Index aggregator service declarations from social.coves.aggregator.service records
8-- Source: Aggregator's own repository (at://aggregator_did/social.coves.aggregator.service/self)
9-- ============================================================================
10CREATE TABLE aggregators (
11 -- Primary identity
12 did TEXT PRIMARY KEY, -- Aggregator's DID (must match repo DID)
13
14 -- Service metadata (from lexicon)
15 display_name TEXT NOT NULL, -- Human-readable name
16 description TEXT, -- What this aggregator does
17 config_schema JSONB, -- JSON Schema for community config validation
18 avatar_url TEXT, -- Avatar image URL (extracted from blob)
19 source_url TEXT, -- URL to source code (transparency)
20 maintainer_did TEXT, -- DID of maintainer
21
22 -- atProto record metadata
23 record_uri TEXT NOT NULL UNIQUE, -- AT-URI of service declaration record
24 record_cid TEXT NOT NULL, -- CID of current record version
25 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When the aggregator service was created (from lexicon createdAt field)
26 indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When indexed/updated by AppView
27
28 -- Cached stats (updated by aggregator_posts table triggers/queries)
29 communities_using INTEGER NOT NULL DEFAULT 0, -- Count of communities with enabled authorizations
30 posts_created BIGINT NOT NULL DEFAULT 0 -- Total posts created by this aggregator
31);
32
33-- Indexes for discovery and lookups
34CREATE INDEX idx_aggregators_created_at ON aggregators(created_at DESC);
35CREATE INDEX idx_aggregators_indexed_at ON aggregators(indexed_at DESC);
36CREATE INDEX idx_aggregators_maintainer ON aggregators(maintainer_did);
37
38-- Comments
39COMMENT ON TABLE aggregators IS 'Aggregator service declarations indexed from social.coves.aggregator.service records';
40COMMENT ON COLUMN aggregators.did IS 'DID of the aggregator service (matches repo DID)';
41COMMENT ON COLUMN aggregators.config_schema IS 'JSON Schema defining what config options communities can set';
42COMMENT ON COLUMN aggregators.created_at IS 'When the aggregator service was created (from lexicon record createdAt field)';
43COMMENT ON COLUMN aggregators.communities_using IS 'Cached count of communities with enabled=true authorizations';
44
45
46-- ============================================================================
47-- Table: aggregator_authorizations
48-- Purpose: Index community authorization records for aggregators
49-- Source: Community's repository (at://community_did/social.coves.aggregator.authorization/rkey)
50-- ============================================================================
51CREATE TABLE aggregator_authorizations (
52 id BIGSERIAL PRIMARY KEY,
53
54 -- Authorization identity
55 aggregator_did TEXT NOT NULL, -- DID of authorized aggregator
56 community_did TEXT NOT NULL, -- DID of community granting access
57
58 -- Authorization state
59 enabled BOOLEAN NOT NULL DEFAULT true, -- Whether aggregator is currently active
60 config JSONB, -- Community-specific config (validated against aggregator's schema)
61
62 -- Audit trail (from lexicon)
63 created_at TIMESTAMPTZ NOT NULL, -- When authorization was created
64 created_by TEXT NOT NULL, -- DID of moderator who authorized (set by API, not client)
65 disabled_at TIMESTAMPTZ, -- When authorization was disabled (if enabled=false)
66 disabled_by TEXT, -- DID of moderator who disabled
67
68 -- atProto record metadata
69 record_uri TEXT NOT NULL UNIQUE, -- AT-URI of authorization record
70 record_cid TEXT NOT NULL, -- CID of current record version
71 indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When indexed/updated by AppView
72
73 -- Constraints
74 UNIQUE(aggregator_did, community_did), -- One authorization per aggregator per community
75 CONSTRAINT fk_aggregator FOREIGN KEY (aggregator_did) REFERENCES aggregators(did) ON DELETE CASCADE,
76 CONSTRAINT fk_community FOREIGN KEY (community_did) REFERENCES communities(did) ON DELETE CASCADE
77);
78
79-- Indexes for authorization checks (CRITICAL PATH - used on every aggregator post)
80CREATE INDEX idx_aggregator_auth_agg_enabled ON aggregator_authorizations(aggregator_did, enabled) WHERE enabled = true;
81CREATE INDEX idx_aggregator_auth_comm_enabled ON aggregator_authorizations(community_did, enabled) WHERE enabled = true;
82CREATE INDEX idx_aggregator_auth_lookup ON aggregator_authorizations(aggregator_did, community_did, enabled);
83
84-- Indexes for listing/discovery
85CREATE INDEX idx_aggregator_auth_agg_did ON aggregator_authorizations(aggregator_did, created_at DESC);
86CREATE INDEX idx_aggregator_auth_comm_did ON aggregator_authorizations(community_did, created_at DESC);
87
88-- Comments
89COMMENT ON TABLE aggregator_authorizations IS 'Community authorizations for aggregators indexed from social.coves.aggregator.authorization records';
90COMMENT ON COLUMN aggregator_authorizations.config IS 'Community-specific config, validated against aggregators.config_schema';
91COMMENT ON INDEX idx_aggregator_auth_lookup IS 'CRITICAL: Fast lookup for post creation authorization checks';
92
93
94-- ============================================================================
95-- Table: aggregator_posts
96-- Purpose: Track posts created by aggregators for rate limiting and stats
97-- Note: This is AppView-only data, not from lexicon records
98-- ============================================================================
99CREATE TABLE aggregator_posts (
100 id BIGSERIAL PRIMARY KEY,
101
102 -- Post identity
103 aggregator_did TEXT NOT NULL, -- DID of aggregator that created the post
104 community_did TEXT NOT NULL, -- DID of community post was created in
105 post_uri TEXT NOT NULL, -- AT-URI of the post record
106 post_cid TEXT NOT NULL, -- CID of the post
107
108 -- Timestamp
109 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When post was created
110
111 -- Constraints
112 UNIQUE(post_uri), -- Each post tracked once
113 CONSTRAINT fk_aggregator_posts_agg FOREIGN KEY (aggregator_did) REFERENCES aggregators(did) ON DELETE CASCADE,
114 CONSTRAINT fk_aggregator_posts_comm FOREIGN KEY (community_did) REFERENCES communities(did) ON DELETE CASCADE
115);
116
117-- Indexes for rate limiting queries (CRITICAL PATH - used on every aggregator post)
118CREATE INDEX idx_aggregator_posts_rate_limit ON aggregator_posts(aggregator_did, community_did, created_at DESC);
119
120-- Indexes for stats
121CREATE INDEX idx_aggregator_posts_agg_did ON aggregator_posts(aggregator_did, created_at DESC);
122CREATE INDEX idx_aggregator_posts_comm_did ON aggregator_posts(community_did, created_at DESC);
123
124-- Comments
125COMMENT ON TABLE aggregator_posts IS 'AppView-only tracking of posts created by aggregators for rate limiting and stats';
126COMMENT ON INDEX idx_aggregator_posts_rate_limit IS 'CRITICAL: Fast rate limit checks (posts in last hour per community)';
127
128
129-- ============================================================================
130-- Trigger: Update aggregator stats when authorizations change
131-- Purpose: Keep aggregators.communities_using count accurate
132-- ============================================================================
133-- +goose StatementBegin
134CREATE OR REPLACE FUNCTION update_aggregator_communities_count()
135RETURNS TRIGGER AS $$
136BEGIN
137 -- Recalculate communities_using count for affected aggregator
138 IF TG_OP = 'DELETE' THEN
139 UPDATE aggregators
140 SET communities_using = (
141 SELECT COUNT(*)
142 FROM aggregator_authorizations
143 WHERE aggregator_did = OLD.aggregator_did
144 AND enabled = true
145 )
146 WHERE did = OLD.aggregator_did;
147 RETURN OLD;
148 ELSE
149 UPDATE aggregators
150 SET communities_using = (
151 SELECT COUNT(*)
152 FROM aggregator_authorizations
153 WHERE aggregator_did = NEW.aggregator_did
154 AND enabled = true
155 )
156 WHERE did = NEW.aggregator_did;
157 RETURN NEW;
158 END IF;
159END;
160$$ LANGUAGE plpgsql;
161-- +goose StatementEnd
162
163CREATE TRIGGER trigger_update_aggregator_communities_count
164 AFTER INSERT OR UPDATE OR DELETE ON aggregator_authorizations
165 FOR EACH ROW
166 EXECUTE FUNCTION update_aggregator_communities_count();
167
168COMMENT ON FUNCTION update_aggregator_communities_count IS 'Maintains aggregators.communities_using count when authorizations change';
169
170
171-- ============================================================================
172-- Trigger: Update aggregator stats when posts are created
173-- Purpose: Keep aggregators.posts_created count accurate
174-- ============================================================================
175-- +goose StatementBegin
176CREATE OR REPLACE FUNCTION update_aggregator_posts_count()
177RETURNS TRIGGER AS $$
178BEGIN
179 IF TG_OP = 'INSERT' THEN
180 UPDATE aggregators
181 SET posts_created = posts_created + 1
182 WHERE did = NEW.aggregator_did;
183 RETURN NEW;
184 ELSIF TG_OP = 'DELETE' THEN
185 UPDATE aggregators
186 SET posts_created = posts_created - 1
187 WHERE did = OLD.aggregator_did;
188 RETURN OLD;
189 END IF;
190END;
191$$ LANGUAGE plpgsql;
192-- +goose StatementEnd
193
194CREATE TRIGGER trigger_update_aggregator_posts_count
195 AFTER INSERT OR DELETE ON aggregator_posts
196 FOR EACH ROW
197 EXECUTE FUNCTION update_aggregator_posts_count();
198
199COMMENT ON FUNCTION update_aggregator_posts_count IS 'Maintains aggregators.posts_created count when posts are tracked';
200
201
202-- +goose Down
203-- Drop triggers first
204DROP TRIGGER IF EXISTS trigger_update_aggregator_posts_count ON aggregator_posts;
205DROP TRIGGER IF EXISTS trigger_update_aggregator_communities_count ON aggregator_authorizations;
206
207-- Drop functions
208DROP FUNCTION IF EXISTS update_aggregator_posts_count();
209DROP FUNCTION IF EXISTS update_aggregator_communities_count();
210
211-- Drop tables in reverse order (respects foreign keys)
212DROP TABLE IF EXISTS aggregator_posts CASCADE;
213DROP TABLE IF EXISTS aggregator_authorizations CASCADE;
214DROP TABLE IF EXISTS aggregators CASCADE;