···88 "time"
991010 _ "github.com/lib/pq"
1111+ "margin.at/internal/domain"
1112)
12131314type DB struct {
1415 *sql.DB
1516}
16171717-type Annotation struct {
1818- URI string `json:"uri"`
1919- AuthorDID string `json:"authorDid"`
2020- Motivation string `json:"motivation,omitempty"`
2121- BodyValue *string `json:"bodyValue,omitempty"`
2222- BodyFormat *string `json:"bodyFormat,omitempty"`
2323- BodyURI *string `json:"bodyUri,omitempty"`
2424- TargetSource string `json:"targetSource"`
2525- TargetHash string `json:"targetHash"`
2626- TargetTitle *string `json:"targetTitle,omitempty"`
2727- SelectorJSON *string `json:"selector,omitempty"`
2828- TagsJSON *string `json:"tags,omitempty"`
2929- CreatedAt time.Time `json:"createdAt"`
3030- IndexedAt time.Time `json:"indexedAt"`
3131- CID *string `json:"cid,omitempty"`
3232-}
3333-3434-type Selector struct {
3535- Type string `json:"type"`
3636- Exact string `json:"exact,omitempty"`
3737- Prefix string `json:"prefix,omitempty"`
3838- Suffix string `json:"suffix,omitempty"`
3939- Start *int `json:"start,omitempty"`
4040- End *int `json:"end,omitempty"`
4141- Value string `json:"value,omitempty"`
4242-}
4343-4444-type Highlight struct {
4545- URI string `json:"uri"`
4646- AuthorDID string `json:"authorDid"`
4747- TargetSource string `json:"targetSource"`
4848- TargetHash string `json:"targetHash"`
4949- TargetTitle *string `json:"targetTitle,omitempty"`
5050- SelectorJSON *string `json:"selector,omitempty"`
5151- Color *string `json:"color,omitempty"`
5252- TagsJSON *string `json:"tags,omitempty"`
5353- CreatedAt time.Time `json:"createdAt"`
5454- IndexedAt time.Time `json:"indexedAt"`
5555- CID *string `json:"cid,omitempty"`
5656-}
5757-5858-type Bookmark struct {
5959- URI string `json:"uri"`
6060- AuthorDID string `json:"authorDid"`
6161- Source string `json:"source"`
6262- SourceHash string `json:"sourceHash"`
6363- Title *string `json:"title,omitempty"`
6464- Description *string `json:"description,omitempty"`
6565- TagsJSON *string `json:"tags,omitempty"`
6666- CreatedAt time.Time `json:"createdAt"`
6767- IndexedAt time.Time `json:"indexedAt"`
6868- CID *string `json:"cid,omitempty"`
6969-}
7070-7171-type Reply struct {
7272- URI string `json:"uri"`
7373- AuthorDID string `json:"authorDid"`
7474- ParentURI string `json:"parentUri"`
7575- RootURI string `json:"rootUri"`
7676- Text string `json:"text"`
7777- Format *string `json:"format,omitempty"`
7878- CreatedAt time.Time `json:"createdAt"`
7979- IndexedAt time.Time `json:"indexedAt"`
8080- CID *string `json:"cid,omitempty"`
8181-}
8282-8383-type Like struct {
8484- URI string `json:"uri"`
8585- AuthorDID string `json:"authorDid"`
8686- SubjectURI string `json:"subjectUri"`
8787- CreatedAt time.Time `json:"createdAt"`
8888- IndexedAt time.Time `json:"indexedAt"`
8989-}
9090-9191-type Collection struct {
9292- URI string `json:"uri"`
9393- AuthorDID string `json:"authorDid"`
9494- Name string `json:"name"`
9595- Description *string `json:"description,omitempty"`
9696- Icon *string `json:"icon,omitempty"`
9797- CreatedAt time.Time `json:"createdAt"`
9898- IndexedAt time.Time `json:"indexedAt"`
9999-}
100100-101101-type CollectionItem struct {
102102- URI string `json:"uri"`
103103- AuthorDID string `json:"authorDid"`
104104- CollectionURI string `json:"collectionUri"`
105105- AnnotationURI string `json:"annotationUri"`
106106- Position int `json:"position"`
107107- CreatedAt time.Time `json:"createdAt"`
108108- IndexedAt time.Time `json:"indexedAt"`
109109-}
110110-111111-type Notification struct {
112112- ID int `json:"id"`
113113- RecipientDID string `json:"recipientDid"`
114114- ActorDID string `json:"actorDid"`
115115- Type string `json:"type"`
116116- SubjectURI string `json:"subjectUri"`
117117- CreatedAt time.Time `json:"createdAt"`
118118- ReadAt *time.Time `json:"readAt,omitempty"`
119119-}
120120-121121-type APIKey struct {
122122- ID string `json:"id"`
123123- OwnerDID string `json:"ownerDid"`
124124- Name string `json:"name"`
125125- KeyHash string `json:"-"`
126126- CreatedAt time.Time `json:"createdAt"`
127127- LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
128128- URI string `json:"uri"`
129129- CID *string `json:"cid,omitempty"`
130130- IndexedAt time.Time `json:"indexedAt"`
131131-}
132132-133133-type Profile struct {
134134- URI string `json:"uri"`
135135- AuthorDID string `json:"authorDid"`
136136- DisplayName *string `json:"displayName,omitempty"`
137137- Avatar *string `json:"avatar,omitempty"`
138138- Bio *string `json:"bio,omitempty"`
139139- Website *string `json:"website,omitempty"`
140140- LinksJSON *string `json:"links,omitempty"`
141141- CreatedAt time.Time `json:"createdAt"`
142142- IndexedAt time.Time `json:"indexedAt"`
143143- CID *string `json:"cid,omitempty"`
144144-}
145145-146146-type Preferences struct {
147147- URI string `json:"uri"`
148148- AuthorDID string `json:"authorDid"`
149149- ExternalLinkSkippedHostnames *string `json:"externalLinkSkippedHostnames,omitempty"`
150150- SubscribedLabelers *string `json:"subscribedLabelers,omitempty"`
151151- LabelPreferences *string `json:"labelPreferences,omitempty"`
152152- DisableExternalLinkWarning *bool `json:"disableExternalLinkWarning,omitempty"`
153153- CreatedAt time.Time `json:"createdAt"`
154154- IndexedAt time.Time `json:"indexedAt"`
155155- CID *string `json:"cid,omitempty"`
156156-}
157157-158158-type Block struct {
159159- ID int `json:"id"`
160160- ActorDID string `json:"actorDid"`
161161- SubjectDID string `json:"subjectDid"`
162162- CreatedAt time.Time `json:"createdAt"`
163163-}
164164-165165-type Mute struct {
166166- ID int `json:"id"`
167167- ActorDID string `json:"actorDid"`
168168- SubjectDID string `json:"subjectDid"`
169169- CreatedAt time.Time `json:"createdAt"`
170170-}
171171-172172-type ModerationReport struct {
173173- ID int `json:"id"`
174174- ReporterDID string `json:"reporterDid"`
175175- SubjectDID string `json:"subjectDid"`
176176- SubjectURI *string `json:"subjectUri,omitempty"`
177177- ReasonType string `json:"reasonType"`
178178- ReasonText *string `json:"reasonText,omitempty"`
179179- Status string `json:"status"`
180180- CreatedAt time.Time `json:"createdAt"`
181181- ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
182182- ResolvedBy *string `json:"resolvedBy,omitempty"`
183183-}
184184-185185-type ModerationAction struct {
186186- ID int `json:"id"`
187187- ReportID int `json:"reportId"`
188188- ActorDID string `json:"actorDid"`
189189- Action string `json:"action"`
190190- Comment *string `json:"comment,omitempty"`
191191- CreatedAt time.Time `json:"createdAt"`
192192-}
193193-194194-type ContentLabel struct {
195195- ID int `json:"id"`
196196- Src string `json:"src"`
197197- URI string `json:"uri"`
198198- Val string `json:"val"`
199199- Neg bool `json:"neg"`
200200- CreatedBy string `json:"createdBy"`
201201- CreatedAt time.Time `json:"createdAt"`
202202-}
1818+type (
1919+ Note = domain.Note
2020+ Annotation = domain.Annotation
2121+ Selector = domain.Selector
2222+ Highlight = domain.Highlight
2323+ Bookmark = domain.Bookmark
2424+ Reply = domain.Reply
2525+ Like = domain.Like
2626+ Collection = domain.Collection
2727+ CollectionItem = domain.CollectionItem
2828+ Notification = domain.Notification
2929+ APIKey = domain.APIKey
3030+ Profile = domain.Profile
3131+ Preferences = domain.Preferences
3232+ Block = domain.Block
3333+ Mute = domain.Mute
3434+ ModerationReport = domain.ModerationReport
3535+ ModerationAction = domain.ModerationAction
3636+ ContentLabel = domain.ContentLabel
3737+)
2033820439func New(dsn string) (*DB, error) {
20540 if !strings.HasPrefix(dsn, "postgres://") && !strings.HasPrefix(dsn, "postgresql://") {
206206- return nil, fmt.Errorf("only PostgreSQL is supported, DSN must start with postgres:// or postgresql://")
4141+ return nil, fmt.Errorf("only PostgreSQL is supported; DSN must start with postgres:// or postgresql://")
20742 }
20843209209- db, err := sql.Open("postgres", dsn)
4444+ sqlDB, err := sql.Open("postgres", dsn)
21045 if err != nil {
211211- return nil, fmt.Errorf("failed to open database connection: %w", err)
4646+ return nil, fmt.Errorf("open database: %w", err)
21247 }
21348214214- db.SetMaxOpenConns(25)
215215- db.SetMaxIdleConns(10)
216216- db.SetConnMaxLifetime(5 * time.Minute)
217217- db.SetConnMaxIdleTime(2 * time.Minute)
4949+ sqlDB.SetMaxOpenConns(25)
5050+ sqlDB.SetMaxIdleConns(10)
5151+ sqlDB.SetConnMaxLifetime(5 * time.Minute)
5252+ sqlDB.SetConnMaxIdleTime(2 * time.Minute)
21853219219- if err := db.Ping(); err != nil {
220220- return nil, fmt.Errorf("failed to ping database: %w", err)
5454+ if err := sqlDB.Ping(); err != nil {
5555+ return nil, fmt.Errorf("ping database: %w", err)
22156 }
22257223223- return &DB{DB: db}, nil
5858+ return &DB{DB: sqlDB}, nil
22459}
22560226226-func (db *DB) Migrate() error {
227227- _, err := db.Exec(`
228228- CREATE TABLE IF NOT EXISTS annotations (
229229- uri TEXT PRIMARY KEY,
230230- author_did TEXT NOT NULL,
231231- motivation TEXT,
232232- body_value TEXT,
233233- body_format TEXT DEFAULT 'text/plain',
234234- body_uri TEXT,
235235- target_source TEXT NOT NULL,
236236- target_hash TEXT NOT NULL,
237237- target_title TEXT,
238238- selector_json TEXT,
239239- tags_json TEXT,
240240- created_at TIMESTAMP NOT NULL,
241241- indexed_at TIMESTAMP NOT NULL,
242242- cid TEXT
243243- )`)
244244- if err != nil {
245245- return err
246246- }
247247-248248- db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_target_hash ON annotations(target_hash)`)
249249- db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_target_source ON annotations(target_source)`)
250250- db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_author_did ON annotations(author_did)`)
251251- db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_motivation ON annotations(motivation)`)
252252- db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_created_at ON annotations(created_at DESC)`)
253253- db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_author_created ON annotations(author_did, created_at DESC)`)
254254- db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_uri_pattern ON annotations(uri text_pattern_ops)`)
255255-256256- db.Exec(`CREATE TABLE IF NOT EXISTS highlights (
257257- uri TEXT PRIMARY KEY,
258258- author_did TEXT NOT NULL,
259259- target_source TEXT NOT NULL,
260260- target_hash TEXT NOT NULL,
261261- target_title TEXT,
262262- selector_json TEXT,
263263- color TEXT,
264264- tags_json TEXT,
265265- created_at TIMESTAMP NOT NULL,
266266- indexed_at TIMESTAMP NOT NULL,
267267- cid TEXT
268268- )`)
269269- db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_target_hash ON highlights(target_hash)`)
270270- db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_author_did ON highlights(author_did)`)
271271- db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_created_at ON highlights(created_at DESC)`)
272272- db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_author_created ON highlights(author_did, created_at DESC)`)
273273- db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_uri_pattern ON highlights(uri text_pattern_ops)`)
274274-275275- db.Exec(`CREATE TABLE IF NOT EXISTS bookmarks (
276276- uri TEXT PRIMARY KEY,
277277- author_did TEXT NOT NULL,
278278- source TEXT NOT NULL,
279279- source_hash TEXT NOT NULL,
280280- title TEXT,
281281- description TEXT,
282282- tags_json TEXT,
283283- created_at TIMESTAMP NOT NULL,
284284- indexed_at TIMESTAMP NOT NULL,
285285- cid TEXT
286286- )`)
287287- db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_source_hash ON bookmarks(source_hash)`)
288288- db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_author_did ON bookmarks(author_did)`)
289289- db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_created_at ON bookmarks(created_at DESC)`)
290290- db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_author_created ON bookmarks(author_did, created_at DESC)`)
291291- db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_uri_pattern ON bookmarks(uri text_pattern_ops)`)
292292-293293- db.Exec(`CREATE TABLE IF NOT EXISTS replies (
294294- uri TEXT PRIMARY KEY,
295295- author_did TEXT NOT NULL,
296296- parent_uri TEXT NOT NULL,
297297- root_uri TEXT NOT NULL,
298298- text TEXT NOT NULL,
299299- format TEXT DEFAULT 'text/plain',
300300- created_at TIMESTAMP NOT NULL,
301301- indexed_at TIMESTAMP NOT NULL,
302302- cid TEXT
303303- )`)
304304- db.Exec(`CREATE INDEX IF NOT EXISTS idx_replies_parent_uri ON replies(parent_uri)`)
305305- db.Exec(`CREATE INDEX IF NOT EXISTS idx_replies_root_uri ON replies(root_uri)`)
306306- db.Exec(`CREATE INDEX IF NOT EXISTS idx_replies_created_at ON replies(created_at DESC)`)
307307- db.Exec(`CREATE INDEX IF NOT EXISTS idx_replies_author_did ON replies(author_did)`)
308308-309309- db.Exec(`CREATE TABLE IF NOT EXISTS likes (
310310- uri TEXT PRIMARY KEY,
311311- author_did TEXT NOT NULL,
312312- subject_uri TEXT NOT NULL,
313313- created_at TIMESTAMP NOT NULL,
314314- indexed_at TIMESTAMP NOT NULL
315315- )`)
316316- db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_subject_uri ON likes(subject_uri)`)
317317- db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_author_did ON likes(author_did)`)
318318- db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_author_subject ON likes(author_did, subject_uri)`)
319319-320320- db.Exec(`CREATE TABLE IF NOT EXISTS collections (
321321- uri TEXT PRIMARY KEY,
322322- author_did TEXT NOT NULL,
323323- name TEXT NOT NULL,
324324- description TEXT,
325325- icon TEXT,
326326- created_at TIMESTAMP NOT NULL,
327327- indexed_at TIMESTAMP NOT NULL
328328- )`)
329329- db.Exec(`CREATE INDEX IF NOT EXISTS idx_collections_author_did ON collections(author_did)`)
330330- db.Exec(`CREATE INDEX IF NOT EXISTS idx_collections_created_at ON collections(created_at DESC)`)
331331-332332- db.Exec(`CREATE TABLE IF NOT EXISTS collection_items (
333333- uri TEXT PRIMARY KEY,
334334- author_did TEXT NOT NULL,
335335- collection_uri TEXT NOT NULL,
336336- annotation_uri TEXT NOT NULL,
337337- position INTEGER DEFAULT 0,
338338- created_at TIMESTAMP NOT NULL,
339339- indexed_at TIMESTAMP NOT NULL
340340- )`)
341341- db.Exec(`CREATE INDEX IF NOT EXISTS idx_collection_items_collection ON collection_items(collection_uri)`)
342342- db.Exec(`CREATE INDEX IF NOT EXISTS idx_collection_items_annotation ON collection_items(annotation_uri)`)
343343- db.Exec(`CREATE INDEX IF NOT EXISTS idx_collection_items_created_at ON collection_items(created_at DESC)`)
344344-345345- db.Exec(`CREATE TABLE IF NOT EXISTS sessions (
346346- id TEXT PRIMARY KEY,
347347- did TEXT NOT NULL,
348348- handle TEXT NOT NULL,
349349- access_token TEXT NOT NULL,
350350- refresh_token TEXT NOT NULL,
351351- dpop_key TEXT,
352352- created_at TIMESTAMP NOT NULL,
353353- expires_at TIMESTAMP NOT NULL
354354- )`)
355355- db.Exec(`CREATE INDEX IF NOT EXISTS idx_sessions_did ON sessions(did)`)
356356- db.Exec(`CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at)`)
357357-358358- db.Exec(`CREATE TABLE IF NOT EXISTS edit_history (
359359- id SERIAL PRIMARY KEY,
360360- uri TEXT NOT NULL,
361361- record_type TEXT NOT NULL,
362362- previous_content TEXT NOT NULL,
363363- previous_cid TEXT,
364364- edited_at TIMESTAMP NOT NULL
365365- )`)
366366- db.Exec(`CREATE INDEX IF NOT EXISTS idx_edit_history_uri ON edit_history(uri)`)
367367- db.Exec(`CREATE INDEX IF NOT EXISTS idx_edit_history_edited_at ON edit_history(edited_at DESC)`)
368368-369369- db.Exec(`CREATE TABLE IF NOT EXISTS notifications (
370370- id SERIAL PRIMARY KEY,
371371- recipient_did TEXT NOT NULL,
372372- actor_did TEXT NOT NULL,
373373- type TEXT NOT NULL,
374374- subject_uri TEXT NOT NULL,
375375- created_at TIMESTAMP NOT NULL,
376376- read_at TIMESTAMP
377377- )`)
378378- db.Exec(`CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON notifications(recipient_did)`)
379379- db.Exec(`CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at DESC)`)
380380- db.Exec(`CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications(recipient_did) WHERE read_at IS NULL`)
381381-382382- db.Exec(`CREATE TABLE IF NOT EXISTS api_keys (
383383- id TEXT PRIMARY KEY,
384384- owner_did TEXT NOT NULL,
385385- name TEXT NOT NULL,
386386- key_hash TEXT NOT NULL,
387387- created_at TIMESTAMP NOT NULL,
388388- last_used_at TIMESTAMP,
389389- uri TEXT,
390390- cid TEXT,
391391- indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
392392- )`)
393393- db.Exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_owner ON api_keys(owner_did)`)
394394- db.Exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)`)
395395-396396- db.Exec(`CREATE TABLE IF NOT EXISTS profiles (
397397- uri TEXT PRIMARY KEY,
398398- author_did TEXT NOT NULL,
399399- display_name TEXT,
400400- avatar TEXT,
401401- bio TEXT,
402402- website TEXT,
403403- links_json TEXT,
404404- created_at TIMESTAMP NOT NULL,
405405- indexed_at TIMESTAMP NOT NULL,
406406- cid TEXT
407407- )`)
408408- db.Exec(`CREATE INDEX IF NOT EXISTS idx_profiles_author_did ON profiles(author_did)`)
409409-410410- db.Exec(`CREATE TABLE IF NOT EXISTS preferences (
411411- uri TEXT PRIMARY KEY,
412412- author_did TEXT NOT NULL,
413413- external_link_skipped_hostnames TEXT,
414414- subscribed_labelers TEXT,
415415- label_preferences TEXT,
416416- disable_external_link_warning BOOLEAN,
417417- created_at TIMESTAMP NOT NULL,
418418- indexed_at TIMESTAMP NOT NULL,
419419- cid TEXT
420420- )`)
421421- db.Exec(`CREATE INDEX IF NOT EXISTS idx_preferences_author_did ON preferences(author_did)`)
422422-423423- db.runMigrations()
424424-425425- db.Exec(`CREATE TABLE IF NOT EXISTS cursors (
426426- id TEXT PRIMARY KEY,
427427- last_cursor BIGINT NOT NULL,
428428- updated_at TIMESTAMP NOT NULL
429429- )`)
430430-431431- db.Exec(`CREATE TABLE IF NOT EXISTS blocks (
432432- id SERIAL PRIMARY KEY,
433433- actor_did TEXT NOT NULL,
434434- subject_did TEXT NOT NULL,
435435- created_at TIMESTAMP NOT NULL,
436436- UNIQUE(actor_did, subject_did)
437437- )`)
438438- db.Exec(`CREATE INDEX IF NOT EXISTS idx_blocks_actor ON blocks(actor_did)`)
439439- db.Exec(`CREATE INDEX IF NOT EXISTS idx_blocks_subject ON blocks(subject_did)`)
440440-441441- db.Exec(`CREATE TABLE IF NOT EXISTS mutes (
442442- id SERIAL PRIMARY KEY,
443443- actor_did TEXT NOT NULL,
444444- subject_did TEXT NOT NULL,
445445- created_at TIMESTAMP NOT NULL,
446446- UNIQUE(actor_did, subject_did)
447447- )`)
448448- db.Exec(`CREATE INDEX IF NOT EXISTS idx_mutes_actor ON mutes(actor_did)`)
449449- db.Exec(`CREATE INDEX IF NOT EXISTS idx_mutes_subject ON mutes(subject_did)`)
450450-451451- db.Exec(`CREATE TABLE IF NOT EXISTS moderation_reports (
452452- id SERIAL PRIMARY KEY,
453453- reporter_did TEXT NOT NULL,
454454- subject_did TEXT NOT NULL,
455455- subject_uri TEXT,
456456- reason_type TEXT NOT NULL,
457457- reason_text TEXT,
458458- status TEXT NOT NULL DEFAULT 'pending',
459459- created_at TIMESTAMP NOT NULL,
460460- resolved_at TIMESTAMP,
461461- resolved_by TEXT
462462- )`)
463463- db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_status ON moderation_reports(status)`)
464464- db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_subject ON moderation_reports(subject_did)`)
465465- db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_reporter ON moderation_reports(reporter_did)`)
466466-467467- db.Exec(`CREATE TABLE IF NOT EXISTS moderation_actions (
468468- id SERIAL PRIMARY KEY,
469469- report_id INTEGER NOT NULL,
470470- actor_did TEXT NOT NULL,
471471- action TEXT NOT NULL,
472472- comment TEXT,
473473- created_at TIMESTAMP NOT NULL
474474- )`)
475475- db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_actions_report ON moderation_actions(report_id)`)
476476-477477- db.Exec(`CREATE TABLE IF NOT EXISTS content_labels (
478478- id SERIAL PRIMARY KEY,
479479- src TEXT NOT NULL,
480480- uri TEXT NOT NULL,
481481- val TEXT NOT NULL,
482482- neg INTEGER NOT NULL DEFAULT 0,
483483- created_by TEXT NOT NULL,
484484- created_at TIMESTAMP NOT NULL
485485- )`)
486486- db.Exec(`CREATE INDEX IF NOT EXISTS idx_content_labels_uri ON content_labels(uri)`)
487487- db.Exec(`CREATE INDEX IF NOT EXISTS idx_content_labels_src ON content_labels(src)`)
488488-489489- db.Exec(`CREATE TABLE IF NOT EXISTS publications (
490490- uri TEXT PRIMARY KEY,
491491- author_did TEXT NOT NULL,
492492- url TEXT NOT NULL,
493493- name TEXT NOT NULL,
494494- description TEXT,
495495- show_in_discover BOOLEAN NOT NULL DEFAULT true,
496496- indexed_at TIMESTAMP NOT NULL
497497- )`)
498498- db.Exec(`CREATE INDEX IF NOT EXISTS idx_publications_author ON publications(author_did)`)
499499- db.Exec(`CREATE INDEX IF NOT EXISTS idx_publications_url ON publications(url)`)
500500-501501- db.Exec(`CREATE TABLE IF NOT EXISTS documents (
502502- uri TEXT PRIMARY KEY,
503503- author_did TEXT NOT NULL,
504504- site TEXT NOT NULL,
505505- path TEXT,
506506- title TEXT NOT NULL,
507507- description TEXT,
508508- text_content TEXT,
509509- tags_json TEXT,
510510- canonical_url TEXT,
511511- published_at TIMESTAMP NOT NULL,
512512- indexed_at TIMESTAMP NOT NULL
513513- )`)
514514- db.Exec(`CREATE INDEX IF NOT EXISTS idx_documents_author ON documents(author_did)`)
515515- db.Exec(`CREATE INDEX IF NOT EXISTS idx_documents_site ON documents(site)`)
516516- db.Exec(`CREATE INDEX IF NOT EXISTS idx_documents_canonical ON documents(canonical_url)`)
517517- db.Exec(`CREATE INDEX IF NOT EXISTS idx_documents_published ON documents(published_at DESC)`)
518518-519519- db.runMigrations()
520520-521521- return nil
522522-}
523523-524524-func (db *DB) GetProfilesByDIDs(dids []string) (map[string]*Profile, error) {
525525- if len(dids) == 0 {
526526- return nil, nil
527527- }
528528-529529- placeholders := make([]string, len(dids))
530530- args := make([]interface{}, len(dids))
531531- for i, did := range dids {
532532- placeholders[i] = fmt.Sprintf("$%d", i+1)
533533- args[i] = did
534534- }
535535-536536- query := `SELECT uri, author_did, display_name, bio, avatar, website, links_json, created_at, indexed_at FROM profiles WHERE author_did IN (` + strings.Join(placeholders, ",") + ")"
537537-538538- rows, err := db.Query(query, args...)
539539- if err != nil {
540540- return nil, err
541541- }
542542- defer rows.Close()
543543-544544- profiles := make(map[string]*Profile)
545545- for rows.Next() {
546546- var p Profile
547547- if err := rows.Scan(&p.URI, &p.AuthorDID, &p.DisplayName, &p.Bio, &p.Avatar, &p.Website, &p.LinksJSON, &p.CreatedAt, &p.IndexedAt); err != nil {
548548- continue
549549- }
550550- profiles[p.AuthorDID] = &p
551551- }
552552-553553- return profiles, nil
554554-}
555555-556556-func (db *DB) GetCursor(id string) (int64, error) {
557557- var cursor int64
558558- err := db.QueryRow("SELECT last_cursor FROM cursors WHERE id = $1", id).Scan(&cursor)
559559- if err == sql.ErrNoRows {
560560- return 0, nil
561561- }
562562- if err != nil {
563563- return 0, err
564564- }
565565- return cursor, nil
566566-}
567567-568568-func (db *DB) SetCursor(id string, cursor int64) error {
569569- query := `
570570- INSERT INTO cursors (id, last_cursor, updated_at)
571571- VALUES ($1, $2, $3)
572572- ON CONFLICT(id) DO UPDATE SET
573573- last_cursor = EXCLUDED.last_cursor,
574574- updated_at = EXCLUDED.updated_at
575575- `
576576- _, err := db.Exec(query, id, cursor, time.Now())
577577- return err
578578-}
579579-580580-func (db *DB) GetProfile(did string) (*Profile, error) {
581581- var p Profile
582582- err := db.QueryRow("SELECT uri, author_did, display_name, avatar, bio, website, links_json, created_at, indexed_at FROM profiles WHERE author_did = $1", did).Scan(
583583- &p.URI, &p.AuthorDID, &p.DisplayName, &p.Avatar, &p.Bio, &p.Website, &p.LinksJSON, &p.CreatedAt, &p.IndexedAt,
584584- )
585585- if err == sql.ErrNoRows {
586586- return nil, nil
587587- }
588588- if err != nil {
589589- return nil, err
590590- }
591591- return &p, nil
592592-}
593593-594594-func (db *DB) UpsertProfile(p *Profile) error {
595595- query := `
596596- INSERT INTO profiles (uri, author_did, display_name, avatar, bio, website, links_json, created_at, indexed_at)
597597- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
598598- ON CONFLICT(uri) DO UPDATE SET
599599- display_name = EXCLUDED.display_name,
600600- avatar = EXCLUDED.avatar,
601601- bio = EXCLUDED.bio,
602602- website = EXCLUDED.website,
603603- links_json = EXCLUDED.links_json,
604604- indexed_at = EXCLUDED.indexed_at
605605- `
606606- _, err := db.Exec(query, p.URI, p.AuthorDID, p.DisplayName, p.Avatar, p.Bio, p.Website, p.LinksJSON, p.CreatedAt, p.IndexedAt)
607607- return err
608608-}
609609-610610-func (db *DB) DeleteProfile(uri string) error {
611611- _, err := db.Exec("DELETE FROM profiles WHERE uri = $1", uri)
612612- return err
613613-}
614614-615615-func (db *DB) DeleteAPIKey(id, ownerDID string) (string, error) {
616616- var uri string
617617- err := db.QueryRow("SELECT uri FROM api_keys WHERE id = $1 AND owner_did = $2", id, ownerDID).Scan(&uri)
618618- if err != nil {
619619- if err == sql.ErrNoRows {
620620- return "", nil
621621- }
622622- return "", err
623623- }
624624-625625- _, err = db.Exec("DELETE FROM api_keys WHERE id = $1 AND owner_did = $2", id, ownerDID)
626626- return uri, err
627627-}
628628-629629-func (db *DB) GetPreferences(did string) (*Preferences, error) {
630630- var p Preferences
631631- err := db.QueryRow("SELECT uri, author_did, external_link_skipped_hostnames, subscribed_labelers, label_preferences, disable_external_link_warning, created_at, indexed_at, cid FROM preferences WHERE author_did = $1", did).Scan(
632632- &p.URI, &p.AuthorDID, &p.ExternalLinkSkippedHostnames, &p.SubscribedLabelers, &p.LabelPreferences, &p.DisableExternalLinkWarning, &p.CreatedAt, &p.IndexedAt, &p.CID,
633633- )
634634- if err == sql.ErrNoRows {
635635- return nil, nil
636636- }
637637- if err != nil {
638638- return nil, err
639639- }
640640- return &p, nil
641641-}
642642-643643-func (db *DB) UpsertPreferences(p *Preferences) error {
644644- query := `
645645- INSERT INTO preferences (uri, author_did, external_link_skipped_hostnames, subscribed_labelers, label_preferences, disable_external_link_warning, created_at, indexed_at, cid)
646646- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
647647- ON CONFLICT(uri) DO UPDATE SET
648648- external_link_skipped_hostnames = EXCLUDED.external_link_skipped_hostnames,
649649- subscribed_labelers = EXCLUDED.subscribed_labelers,
650650- label_preferences = EXCLUDED.label_preferences,
651651- disable_external_link_warning = EXCLUDED.disable_external_link_warning,
652652- indexed_at = EXCLUDED.indexed_at,
653653- cid = EXCLUDED.cid
654654- `
655655- _, err := db.Exec(query, p.URI, p.AuthorDID, p.ExternalLinkSkippedHostnames, p.SubscribedLabelers, p.LabelPreferences, p.DisableExternalLinkWarning, p.CreatedAt, p.IndexedAt, p.CID)
656656- return err
657657-}
658658-659659-func (db *DB) DeleteAPIKeyByURI(uri string) error {
660660- _, err := db.Exec("DELETE FROM api_keys WHERE uri = $1", uri)
661661- return err
662662-}
663663-664664-func (db *DB) DeletePreferences(uri string) error {
665665- _, err := db.Exec("DELETE FROM preferences WHERE uri = $1", uri)
666666- return err
667667-}
668668-669669-func (db *DB) GetAPIKeyURIs(ownerDID string) ([]string, error) {
670670- rows, err := db.Query("SELECT uri FROM api_keys WHERE owner_did = $1 AND uri IS NOT NULL AND uri != ''", ownerDID)
671671- if err != nil {
672672- return nil, err
673673- }
674674- defer rows.Close()
675675- var uris []string
676676- for rows.Next() {
677677- var uri string
678678- if err := rows.Scan(&uri); err != nil {
679679- return nil, err
680680- }
681681- uris = append(uris, uri)
682682- }
683683- return uris, nil
684684-}
685685-686686-func (db *DB) GetPreferenceURIs(did string) ([]string, error) {
687687- rows, err := db.Query("SELECT uri FROM preferences WHERE author_did = $1 AND uri IS NOT NULL AND uri != ''", did)
688688- if err != nil {
689689- return nil, err
690690- }
691691- defer rows.Close()
692692- var uris []string
693693- for rows.Next() {
694694- var uri string
695695- if err := rows.Scan(&uri); err != nil {
696696- return nil, err
697697- }
698698- uris = append(uris, uri)
699699- }
700700- return uris, nil
701701-}
702702-703703-func (db *DB) runMigrations() {
704704- db.Exec(`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS dpop_key TEXT`)
705705-706706- db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS motivation TEXT`)
707707- db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS body_value TEXT`)
708708- db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS body_format TEXT DEFAULT 'text/plain'`)
709709- db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS body_uri TEXT`)
710710- db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS target_source TEXT`)
711711- db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS target_hash TEXT`)
712712- db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS target_title TEXT`)
713713- db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS selector_json TEXT`)
714714- db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS tags_json TEXT`)
715715- db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS cid TEXT`)
716716-717717- db.Exec(`UPDATE annotations SET target_source = url WHERE target_source IS NULL AND url IS NOT NULL`)
718718- db.Exec(`UPDATE annotations SET target_hash = url_hash WHERE target_hash IS NULL AND url_hash IS NOT NULL`)
719719- db.Exec(`UPDATE annotations SET body_value = text WHERE body_value IS NULL AND text IS NOT NULL`)
720720- db.Exec(`UPDATE annotations SET target_title = title WHERE target_title IS NULL AND title IS NOT NULL`)
721721- db.Exec(`UPDATE annotations SET motivation = 'commenting' WHERE motivation IS NULL`)
722722-723723- db.Exec(`ALTER TABLE profiles ADD COLUMN IF NOT EXISTS website TEXT`)
724724- db.Exec(`ALTER TABLE profiles ADD COLUMN IF NOT EXISTS display_name TEXT`)
725725- db.Exec(`ALTER TABLE profiles ADD COLUMN IF NOT EXISTS avatar TEXT`)
726726-727727- db.Exec(`ALTER TABLE cursors ALTER COLUMN last_cursor TYPE BIGINT`)
728728-729729- db.Exec(`ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS uri TEXT`)
730730- db.Exec(`ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS cid TEXT`)
731731- db.Exec(`ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`)
732732-733733- db.migrateModeration()
734734-735735- db.Exec(`ALTER TABLE preferences ADD COLUMN IF NOT EXISTS subscribed_labelers TEXT`)
736736- db.Exec(`ALTER TABLE preferences ADD COLUMN IF NOT EXISTS label_preferences TEXT`)
737737- db.Exec(`ALTER TABLE preferences ADD COLUMN IF NOT EXISTS disable_external_link_warning BOOLEAN`)
738738-}
739739-740740-func (db *DB) migrateModeration() {
741741- _, err := db.Exec(`SELECT subject_did FROM moderation_reports LIMIT 0`)
742742- if err != nil {
743743- db.Exec(`DROP TABLE IF EXISTS moderation_reports`)
744744- db.Exec(`DROP TABLE IF EXISTS moderation_actions`)
745745-746746- db.Exec(`CREATE TABLE IF NOT EXISTS moderation_reports (
747747- id SERIAL PRIMARY KEY,
748748- reporter_did TEXT NOT NULL,
749749- subject_did TEXT NOT NULL,
750750- subject_uri TEXT,
751751- reason_type TEXT NOT NULL,
752752- reason_text TEXT,
753753- status TEXT NOT NULL DEFAULT 'pending',
754754- created_at TIMESTAMP NOT NULL,
755755- resolved_at TIMESTAMP,
756756- resolved_by TEXT
757757- )`)
758758- db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_status ON moderation_reports(status)`)
759759- db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_subject ON moderation_reports(subject_did)`)
760760- db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_reporter ON moderation_reports(reporter_did)`)
761761-762762- db.Exec(`CREATE TABLE IF NOT EXISTS moderation_actions (
763763- id SERIAL PRIMARY KEY,
764764- report_id INTEGER NOT NULL,
765765- actor_did TEXT NOT NULL,
766766- action TEXT NOT NULL,
767767- comment TEXT,
768768- created_at TIMESTAMP NOT NULL
769769- )`)
770770- db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_actions_report ON moderation_actions(report_id)`)
771771- }
772772-773773- db.Exec(`CREATE TABLE IF NOT EXISTS content_labels (
774774- id SERIAL PRIMARY KEY,
775775- src TEXT NOT NULL,
776776- uri TEXT NOT NULL,
777777- val TEXT NOT NULL,
778778- neg INTEGER NOT NULL DEFAULT 0,
779779- created_by TEXT NOT NULL,
780780- created_at TIMESTAMP NOT NULL
781781- )`)
782782- db.Exec(`CREATE INDEX IF NOT EXISTS idx_content_labels_uri ON content_labels(uri)`)
783783- db.Exec(`CREATE INDEX IF NOT EXISTS idx_content_labels_src ON content_labels(src)`)
784784-}
785785-786786-func (db *DB) Close() error {
787787- return db.DB.Close()
788788-}
6161+func (db *DB) Close() error { return db.DB.Close() }
7896279063func ParseSelector(selectorJSON *string) (*Selector, error) {
79164 if selectorJSON == nil || *selectorJSON == "" {
79265 return nil, nil
79366 }
79467 var s Selector
795795- err := json.Unmarshal([]byte(*selectorJSON), &s)
796796- if err != nil {
6868+ if err := json.Unmarshal([]byte(*selectorJSON), &s); err != nil {
79769 return nil, err
79870 }
79971 return &s, nil
···80476 return nil, nil
80577 }
80678 var tags []string
807807- err := json.Unmarshal([]byte(*tagsJSON), &tags)
808808- if err != nil {
7979+ if err := json.Unmarshal([]byte(*tagsJSON), &tags); err != nil {
80980 return nil, err
81081 }
81182 return tags, nil
+230
backend/internal/db/filter.go
···11+package db
22+33+import (
44+ "database/sql"
55+ "fmt"
66+ "strings"
77+ "time"
88+99+ "margin.at/internal/domain"
1010+)
1111+1212+type FeedType = domain.FeedType
1313+type NoteFilter = domain.NoteFilter
1414+1515+const (
1616+ FeedTypeRecent = domain.FeedTypeRecent
1717+ FeedTypePopular = domain.FeedTypePopular
1818+ FeedTypeShelved = domain.FeedTypeShelved
1919+ FeedTypeMargin = domain.FeedTypeMargin
2020+ FeedTypeSemble = domain.FeedTypeSemble
2121+)
2222+2323+func (db *DB) ListNotes(f NoteFilter) ([]Note, error) {
2424+ var where []string
2525+ var args []interface{}
2626+ n := 1
2727+2828+ if len(f.Motivations) == 1 {
2929+ where = append(where, fmt.Sprintf("motivation = $%d", n))
3030+ args = append(args, f.Motivations[0])
3131+ n++
3232+ } else if len(f.Motivations) > 1 {
3333+ placeholders := make([]string, len(f.Motivations))
3434+ for i, m := range f.Motivations {
3535+ placeholders[i] = fmt.Sprintf("$%d", n)
3636+ args = append(args, m)
3737+ n++
3838+ }
3939+ where = append(where, fmt.Sprintf("motivation IN (%s)", strings.Join(placeholders, ", ")))
4040+ }
4141+4242+ if f.AuthorDID != "" {
4343+ where = append(where, fmt.Sprintf("author_did = $%d", n))
4444+ args = append(args, f.AuthorDID)
4545+ n++
4646+ }
4747+4848+ if f.TargetHash != "" {
4949+ where = append(where, fmt.Sprintf("target_hash = $%d", n))
5050+ args = append(args, f.TargetHash)
5151+ n++
5252+ }
5353+5454+ if f.Tag != "" {
5555+ where = append(where, fmt.Sprintf(
5656+ "tags_json IS NOT NULL AND EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = lower($%d))",
5757+ n,
5858+ ))
5959+ args = append(args, f.Tag)
6060+ n++
6161+ }
6262+6363+ if f.Query != "" {
6464+ pattern := "%" + escapeLike(f.Query) + "%"
6565+ where = append(where, fmt.Sprintf(
6666+ "(body_value ILIKE $%d OR target_source ILIKE $%d OR target_title ILIKE $%d OR tags_json::text ILIKE $%d)",
6767+ n, n+1, n+2, n+3,
6868+ ))
6969+ args = append(args, pattern, pattern, pattern, pattern)
7070+ n += 4
7171+ }
7272+7373+ switch f.FeedType {
7474+ case FeedTypeMargin:
7575+ where = append(where, "uri NOT LIKE '%network.cosmik%'")
7676+ case FeedTypeSemble:
7777+ where = append(where, "uri LIKE '%network.cosmik%'")
7878+ case FeedTypePopular:
7979+ since := time.Now().AddDate(0, 0, -14)
8080+ where = append(where, fmt.Sprintf("created_at > $%d", n))
8181+ args = append(args, since)
8282+ n++
8383+ case FeedTypeShelved:
8484+ olderThan := time.Now().AddDate(0, 0, -1)
8585+ since := time.Now().AddDate(0, 0, -14)
8686+ where = append(where, fmt.Sprintf("created_at < $%d AND created_at > $%d", n, n+1))
8787+ where = append(where, "NOT EXISTS (SELECT 1 FROM likes WHERE subject_uri = uri)")
8888+ where = append(where, "NOT EXISTS (SELECT 1 FROM replies WHERE root_uri = uri)")
8989+ args = append(args, olderThan, since)
9090+ n += 2
9191+ }
9292+9393+ whereClause := ""
9494+ if len(where) > 0 {
9595+ whereClause = "WHERE " + strings.Join(where, " AND ")
9696+ }
9797+9898+ orderClause := "ORDER BY created_at DESC"
9999+ switch f.FeedType {
100100+ case FeedTypePopular:
101101+ orderClause = `ORDER BY (
102102+ SELECT COUNT(*) FROM likes WHERE subject_uri = uri
103103+ ) + (
104104+ SELECT COUNT(*) FROM replies WHERE root_uri = uri
105105+ ) DESC, created_at DESC`
106106+ case FeedTypeShelved:
107107+ orderClause = "ORDER BY RANDOM()"
108108+ }
109109+110110+ limit := f.Limit
111111+ if limit <= 0 {
112112+ limit = 50
113113+ }
114114+115115+ query := fmt.Sprintf(`
116116+ SELECT uri, author_did, motivation, color, description, body_value, body_format, body_uri,
117117+ target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
118118+ FROM unified_notes
119119+ %s
120120+ %s
121121+ LIMIT $%d OFFSET $%d
122122+ `, whereClause, orderClause, n, n+1)
123123+124124+ args = append(args, limit, f.Offset)
125125+126126+ rows, err := db.Query(query, args...)
127127+ if err != nil {
128128+ return nil, err
129129+ }
130130+ defer rows.Close()
131131+ return scanNotes(rows)
132132+}
133133+134134+func (db *DB) GetNoteByURIFromUnified(uri string) (*Note, error) {
135135+ query := `
136136+ SELECT uri, author_did, motivation, color, description, body_value, body_format, body_uri,
137137+ target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
138138+ FROM unified_notes
139139+ WHERE uri = $1
140140+ `
141141+ var note Note
142142+ err := db.QueryRow(query, uri).Scan(
143143+ ¬e.URI, ¬e.AuthorDID, ¬e.Motivation, ¬e.Color, ¬e.Description,
144144+ ¬e.BodyValue, ¬e.BodyFormat, ¬e.BodyURI,
145145+ ¬e.TargetSource, ¬e.TargetHash, ¬e.TargetTitle,
146146+ ¬e.SelectorJSON, ¬e.TagsJSON, ¬e.CreatedAt, ¬e.IndexedAt, ¬e.CID,
147147+ )
148148+ if err == sql.ErrNoRows {
149149+ return nil, nil
150150+ }
151151+ if err != nil {
152152+ return nil, err
153153+ }
154154+ return ¬e, nil
155155+}
156156+157157+func scanNotes(rows interface {
158158+ Next() bool
159159+ Scan(...interface{}) error
160160+}) ([]Note, error) {
161161+ var notes []Note
162162+ for rows.Next() {
163163+ var note Note
164164+ if err := rows.Scan(
165165+ ¬e.URI, ¬e.AuthorDID, ¬e.Motivation, ¬e.Color, ¬e.Description,
166166+ ¬e.BodyValue, ¬e.BodyFormat, ¬e.BodyURI,
167167+ ¬e.TargetSource, ¬e.TargetHash, ¬e.TargetTitle,
168168+ ¬e.SelectorJSON, ¬e.TagsJSON, ¬e.CreatedAt, ¬e.IndexedAt, ¬e.CID,
169169+ ); err != nil {
170170+ return nil, err
171171+ }
172172+ notes = append(notes, note)
173173+ }
174174+ return notes, nil
175175+}
176176+177177+func (db *DB) MigrateUnifiedNotes() {
178178+ db.Exec(`
179179+ CREATE OR REPLACE VIEW unified_notes AS
180180+ -- New notes table (primary path)
181181+ SELECT
182182+ uri, author_did,
183183+ COALESCE(motivation, 'commenting') AS motivation,
184184+ color, description, body_value, body_format, body_uri,
185185+ target_source, target_hash, target_title, selector_json, tags_json,
186186+ created_at, indexed_at, cid
187187+ FROM notes
188188+ UNION ALL
189189+ -- Legacy annotations (motivation is 'commenting' or similar, never 'highlighting'/'bookmarking')
190190+ SELECT
191191+ uri, author_did,
192192+ COALESCE(motivation, 'commenting') AS motivation,
193193+ NULL::TEXT AS color,
194194+ NULL::TEXT AS description,
195195+ body_value, body_format, body_uri,
196196+ target_source, target_hash, target_title, selector_json, tags_json,
197197+ created_at, indexed_at, cid
198198+ FROM annotations
199199+ UNION ALL
200200+ -- Legacy highlights
201201+ SELECT
202202+ uri, author_did,
203203+ 'highlighting' AS motivation,
204204+ color,
205205+ NULL::TEXT AS description,
206206+ NULL::TEXT AS body_value,
207207+ 'text/plain' AS body_format,
208208+ NULL::TEXT AS body_uri,
209209+ target_source, target_hash, target_title, selector_json, tags_json,
210210+ created_at, indexed_at, cid
211211+ FROM highlights
212212+ UNION ALL
213213+ -- Legacy bookmarks (column mapping to Note layout)
214214+ SELECT
215215+ uri, author_did,
216216+ 'bookmarking' AS motivation,
217217+ NULL::TEXT AS color,
218218+ description,
219219+ NULL::TEXT AS body_value,
220220+ 'text/plain' AS body_format,
221221+ NULL::TEXT AS body_uri,
222222+ source AS target_source,
223223+ source_hash AS target_hash,
224224+ title AS target_title,
225225+ NULL::TEXT AS selector_json,
226226+ tags_json,
227227+ created_at, indexed_at, cid
228228+ FROM bookmarks
229229+ `)
230230+}
···11+-- +goose Up
22+CREATE TABLE IF NOT EXISTS notes (
33+ uri TEXT PRIMARY KEY,
44+ author_did TEXT NOT NULL,
55+ motivation TEXT,
66+ color TEXT,
77+ description TEXT,
88+ body_value TEXT,
99+ body_format TEXT DEFAULT 'text/plain',
1010+ body_uri TEXT,
1111+ target_source TEXT NOT NULL,
1212+ target_hash TEXT NOT NULL,
1313+ target_title TEXT,
1414+ selector_json TEXT,
1515+ tags_json TEXT,
1616+ created_at TIMESTAMP NOT NULL,
1717+ indexed_at TIMESTAMP NOT NULL,
1818+ cid TEXT
1919+);
2020+CREATE INDEX IF NOT EXISTS idx_notes_target_hash ON notes(target_hash);
2121+CREATE INDEX IF NOT EXISTS idx_notes_target_source ON notes(target_source);
2222+CREATE INDEX IF NOT EXISTS idx_notes_author_did ON notes(author_did);
2323+CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at DESC);
2424+2525+CREATE TABLE IF NOT EXISTS annotations (
2626+ uri TEXT PRIMARY KEY,
2727+ author_did TEXT NOT NULL,
2828+ motivation TEXT,
2929+ body_value TEXT,
3030+ body_format TEXT DEFAULT 'text/plain',
3131+ body_uri TEXT,
3232+ target_source TEXT,
3333+ target_hash TEXT,
3434+ target_title TEXT,
3535+ selector_json TEXT,
3636+ tags_json TEXT,
3737+ created_at TIMESTAMP NOT NULL,
3838+ indexed_at TIMESTAMP NOT NULL,
3939+ cid TEXT
4040+);
4141+CREATE INDEX IF NOT EXISTS idx_annotations_target_hash ON annotations(target_hash);
4242+CREATE INDEX IF NOT EXISTS idx_annotations_target_source ON annotations(target_source);
4343+CREATE INDEX IF NOT EXISTS idx_annotations_author_did ON annotations(author_did);
4444+CREATE INDEX IF NOT EXISTS idx_annotations_motivation ON annotations(motivation);
4545+CREATE INDEX IF NOT EXISTS idx_annotations_created_at ON annotations(created_at DESC);
4646+CREATE INDEX IF NOT EXISTS idx_annotations_author_created ON annotations(author_did, created_at DESC);
4747+CREATE INDEX IF NOT EXISTS idx_annotations_uri_pattern ON annotations(uri text_pattern_ops);
4848+4949+CREATE TABLE IF NOT EXISTS highlights (
5050+ uri TEXT PRIMARY KEY,
5151+ author_did TEXT NOT NULL,
5252+ target_source TEXT NOT NULL,
5353+ target_hash TEXT NOT NULL,
5454+ target_title TEXT,
5555+ selector_json TEXT,
5656+ color TEXT,
5757+ tags_json TEXT,
5858+ created_at TIMESTAMP NOT NULL,
5959+ indexed_at TIMESTAMP NOT NULL,
6060+ cid TEXT
6161+);
6262+CREATE INDEX IF NOT EXISTS idx_highlights_target_hash ON highlights(target_hash);
6363+CREATE INDEX IF NOT EXISTS idx_highlights_author_did ON highlights(author_did);
6464+CREATE INDEX IF NOT EXISTS idx_highlights_created_at ON highlights(created_at DESC);
6565+CREATE INDEX IF NOT EXISTS idx_highlights_author_created ON highlights(author_did, created_at DESC);
6666+CREATE INDEX IF NOT EXISTS idx_highlights_uri_pattern ON highlights(uri text_pattern_ops);
6767+6868+CREATE TABLE IF NOT EXISTS bookmarks (
6969+ uri TEXT PRIMARY KEY,
7070+ author_did TEXT NOT NULL,
7171+ source TEXT NOT NULL,
7272+ source_hash TEXT NOT NULL,
7373+ title TEXT,
7474+ description TEXT,
7575+ tags_json TEXT,
7676+ created_at TIMESTAMP NOT NULL,
7777+ indexed_at TIMESTAMP NOT NULL,
7878+ cid TEXT
7979+);
8080+CREATE INDEX IF NOT EXISTS idx_bookmarks_source_hash ON bookmarks(source_hash);
8181+CREATE INDEX IF NOT EXISTS idx_bookmarks_author_did ON bookmarks(author_did);
8282+CREATE INDEX IF NOT EXISTS idx_bookmarks_created_at ON bookmarks(created_at DESC);
8383+CREATE INDEX IF NOT EXISTS idx_bookmarks_author_created ON bookmarks(author_did, created_at DESC);
8484+CREATE INDEX IF NOT EXISTS idx_bookmarks_uri_pattern ON bookmarks(uri text_pattern_ops);
8585+8686+CREATE TABLE IF NOT EXISTS replies (
8787+ uri TEXT PRIMARY KEY,
8888+ author_did TEXT NOT NULL,
8989+ parent_uri TEXT NOT NULL,
9090+ root_uri TEXT NOT NULL,
9191+ text TEXT NOT NULL,
9292+ format TEXT DEFAULT 'text/plain',
9393+ created_at TIMESTAMP NOT NULL,
9494+ indexed_at TIMESTAMP NOT NULL,
9595+ cid TEXT
9696+);
9797+CREATE INDEX IF NOT EXISTS idx_replies_parent_uri ON replies(parent_uri);
9898+CREATE INDEX IF NOT EXISTS idx_replies_root_uri ON replies(root_uri);
9999+CREATE INDEX IF NOT EXISTS idx_replies_created_at ON replies(created_at DESC);
100100+CREATE INDEX IF NOT EXISTS idx_replies_author_did ON replies(author_did);
101101+102102+CREATE TABLE IF NOT EXISTS likes (
103103+ uri TEXT PRIMARY KEY,
104104+ author_did TEXT NOT NULL,
105105+ subject_uri TEXT NOT NULL,
106106+ created_at TIMESTAMP NOT NULL,
107107+ indexed_at TIMESTAMP NOT NULL
108108+);
109109+CREATE INDEX IF NOT EXISTS idx_likes_subject_uri ON likes(subject_uri);
110110+CREATE INDEX IF NOT EXISTS idx_likes_author_did ON likes(author_did);
111111+CREATE INDEX IF NOT EXISTS idx_likes_author_subject ON likes(author_did, subject_uri);
112112+113113+CREATE TABLE IF NOT EXISTS sessions (
114114+ id TEXT PRIMARY KEY,
115115+ did TEXT NOT NULL,
116116+ handle TEXT NOT NULL,
117117+ access_token TEXT NOT NULL,
118118+ refresh_token TEXT NOT NULL,
119119+ dpop_key TEXT,
120120+ created_at TIMESTAMP NOT NULL,
121121+ expires_at TIMESTAMP NOT NULL
122122+);
123123+CREATE INDEX IF NOT EXISTS idx_sessions_did ON sessions(did);
124124+CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
125125+126126+CREATE TABLE IF NOT EXISTS edit_history (
127127+ id SERIAL PRIMARY KEY,
128128+ uri TEXT NOT NULL,
129129+ record_type TEXT NOT NULL,
130130+ previous_content TEXT NOT NULL,
131131+ previous_cid TEXT,
132132+ edited_at TIMESTAMP NOT NULL
133133+);
134134+CREATE INDEX IF NOT EXISTS idx_edit_history_uri ON edit_history(uri);
135135+CREATE INDEX IF NOT EXISTS idx_edit_history_edited_at ON edit_history(edited_at DESC);
136136+137137+CREATE TABLE IF NOT EXISTS notifications (
138138+ id SERIAL PRIMARY KEY,
139139+ recipient_did TEXT NOT NULL,
140140+ actor_did TEXT NOT NULL,
141141+ type TEXT NOT NULL,
142142+ subject_uri TEXT NOT NULL,
143143+ created_at TIMESTAMP NOT NULL,
144144+ read_at TIMESTAMP
145145+);
146146+CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON notifications(recipient_did);
147147+CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at DESC);
148148+CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications(recipient_did) WHERE read_at IS NULL;
149149+150150+CREATE TABLE IF NOT EXISTS api_keys (
151151+ id TEXT PRIMARY KEY,
152152+ owner_did TEXT NOT NULL,
153153+ name TEXT NOT NULL,
154154+ key_hash TEXT NOT NULL,
155155+ created_at TIMESTAMP NOT NULL,
156156+ last_used_at TIMESTAMP,
157157+ uri TEXT,
158158+ cid TEXT,
159159+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
160160+);
161161+CREATE INDEX IF NOT EXISTS idx_api_keys_owner ON api_keys(owner_did);
162162+CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash);
163163+164164+CREATE TABLE IF NOT EXISTS profiles (
165165+ uri TEXT PRIMARY KEY,
166166+ author_did TEXT NOT NULL,
167167+ display_name TEXT,
168168+ avatar TEXT,
169169+ bio TEXT,
170170+ website TEXT,
171171+ links_json TEXT,
172172+ created_at TIMESTAMP NOT NULL,
173173+ indexed_at TIMESTAMP NOT NULL,
174174+ cid TEXT
175175+);
176176+CREATE INDEX IF NOT EXISTS idx_profiles_author_did ON profiles(author_did);
177177+178178+CREATE TABLE IF NOT EXISTS preferences (
179179+ uri TEXT PRIMARY KEY,
180180+ author_did TEXT NOT NULL,
181181+ external_link_skipped_hostnames TEXT,
182182+ subscribed_labelers TEXT,
183183+ label_preferences TEXT,
184184+ disable_external_link_warning BOOLEAN,
185185+ enable_community_bookmarks BOOLEAN DEFAULT false,
186186+ created_at TIMESTAMP NOT NULL,
187187+ indexed_at TIMESTAMP NOT NULL,
188188+ cid TEXT
189189+);
190190+CREATE INDEX IF NOT EXISTS idx_preferences_author_did ON preferences(author_did);
191191+192192+-- +goose Down
193193+DROP TABLE IF EXISTS preferences;
194194+DROP TABLE IF EXISTS profiles;
195195+DROP TABLE IF EXISTS api_keys;
196196+DROP TABLE IF EXISTS notifications;
197197+DROP TABLE IF EXISTS edit_history;
198198+DROP TABLE IF EXISTS sessions;
199199+DROP TABLE IF EXISTS likes;
200200+DROP TABLE IF EXISTS replies;
201201+DROP TABLE IF EXISTS bookmarks;
202202+DROP TABLE IF EXISTS highlights;
203203+DROP TABLE IF EXISTS annotations;
204204+DROP TABLE IF EXISTS notes;
···11+-- +goose Up
22+CREATE TABLE IF NOT EXISTS cursors (
33+ id TEXT PRIMARY KEY,
44+ last_cursor BIGINT NOT NULL,
55+ updated_at TIMESTAMP NOT NULL
66+);
77+88+CREATE TABLE IF NOT EXISTS collections (
99+ uri TEXT PRIMARY KEY,
1010+ author_did TEXT NOT NULL,
1111+ name TEXT NOT NULL,
1212+ description TEXT,
1313+ icon TEXT,
1414+ created_at TIMESTAMP NOT NULL,
1515+ indexed_at TIMESTAMP NOT NULL
1616+);
1717+CREATE INDEX IF NOT EXISTS idx_collections_author_did ON collections(author_did);
1818+CREATE INDEX IF NOT EXISTS idx_collections_created_at ON collections(created_at DESC);
1919+2020+CREATE TABLE IF NOT EXISTS collection_items (
2121+ uri TEXT PRIMARY KEY,
2222+ author_did TEXT NOT NULL,
2323+ collection_uri TEXT NOT NULL,
2424+ annotation_uri TEXT NOT NULL,
2525+ position INTEGER DEFAULT 0,
2626+ created_at TIMESTAMP NOT NULL,
2727+ indexed_at TIMESTAMP NOT NULL
2828+);
2929+CREATE INDEX IF NOT EXISTS idx_collection_items_collection ON collection_items(collection_uri);
3030+CREATE INDEX IF NOT EXISTS idx_collection_items_annotation ON collection_items(annotation_uri);
3131+CREATE INDEX IF NOT EXISTS idx_collection_items_created_at ON collection_items(created_at DESC);
3232+3333+CREATE TABLE IF NOT EXISTS blocks (
3434+ id SERIAL PRIMARY KEY,
3535+ actor_did TEXT NOT NULL,
3636+ subject_did TEXT NOT NULL,
3737+ created_at TIMESTAMP NOT NULL,
3838+ UNIQUE(actor_did, subject_did)
3939+);
4040+CREATE INDEX IF NOT EXISTS idx_blocks_actor ON blocks(actor_did);
4141+CREATE INDEX IF NOT EXISTS idx_blocks_subject ON blocks(subject_did);
4242+4343+CREATE TABLE IF NOT EXISTS mutes (
4444+ id SERIAL PRIMARY KEY,
4545+ actor_did TEXT NOT NULL,
4646+ subject_did TEXT NOT NULL,
4747+ created_at TIMESTAMP NOT NULL,
4848+ UNIQUE(actor_did, subject_did)
4949+);
5050+CREATE INDEX IF NOT EXISTS idx_mutes_actor ON mutes(actor_did);
5151+CREATE INDEX IF NOT EXISTS idx_mutes_subject ON mutes(subject_did);
5252+5353+CREATE TABLE IF NOT EXISTS content_labels (
5454+ id SERIAL PRIMARY KEY,
5555+ src TEXT NOT NULL,
5656+ uri TEXT NOT NULL,
5757+ val TEXT NOT NULL,
5858+ neg INTEGER NOT NULL DEFAULT 0,
5959+ created_by TEXT NOT NULL,
6060+ created_at TIMESTAMP NOT NULL
6161+);
6262+CREATE INDEX IF NOT EXISTS idx_content_labels_uri ON content_labels(uri);
6363+CREATE INDEX IF NOT EXISTS idx_content_labels_src ON content_labels(src);
6464+6565+CREATE TABLE IF NOT EXISTS moderation_reports (
6666+ id SERIAL PRIMARY KEY,
6767+ reporter_did TEXT NOT NULL,
6868+ subject_did TEXT NOT NULL,
6969+ subject_uri TEXT,
7070+ reason_type TEXT NOT NULL,
7171+ reason_text TEXT,
7272+ status TEXT NOT NULL DEFAULT 'pending',
7373+ created_at TIMESTAMP NOT NULL,
7474+ resolved_at TIMESTAMP,
7575+ resolved_by TEXT
7676+);
7777+CREATE INDEX IF NOT EXISTS idx_mod_reports_status ON moderation_reports(status);
7878+CREATE INDEX IF NOT EXISTS idx_mod_reports_subject ON moderation_reports(subject_did);
7979+CREATE INDEX IF NOT EXISTS idx_mod_reports_reporter ON moderation_reports(reporter_did);
8080+8181+CREATE TABLE IF NOT EXISTS moderation_actions (
8282+ id SERIAL PRIMARY KEY,
8383+ report_id INTEGER NOT NULL,
8484+ actor_did TEXT NOT NULL,
8585+ action TEXT NOT NULL,
8686+ comment TEXT,
8787+ created_at TIMESTAMP NOT NULL
8888+);
8989+CREATE INDEX IF NOT EXISTS idx_mod_actions_report ON moderation_actions(report_id);
9090+9191+CREATE TABLE IF NOT EXISTS publications (
9292+ uri TEXT PRIMARY KEY,
9393+ author_did TEXT NOT NULL,
9494+ url TEXT NOT NULL,
9595+ name TEXT NOT NULL,
9696+ description TEXT,
9797+ show_in_discover BOOLEAN NOT NULL DEFAULT true,
9898+ indexed_at TIMESTAMP NOT NULL
9999+);
100100+CREATE INDEX IF NOT EXISTS idx_publications_author ON publications(author_did);
101101+CREATE INDEX IF NOT EXISTS idx_publications_url ON publications(url);
102102+103103+CREATE TABLE IF NOT EXISTS documents (
104104+ uri TEXT PRIMARY KEY,
105105+ author_did TEXT NOT NULL,
106106+ site TEXT NOT NULL,
107107+ path TEXT,
108108+ title TEXT NOT NULL,
109109+ description TEXT,
110110+ text_content TEXT,
111111+ tags_json TEXT,
112112+ canonical_url TEXT,
113113+ published_at TIMESTAMP NOT NULL,
114114+ indexed_at TIMESTAMP NOT NULL
115115+);
116116+CREATE INDEX IF NOT EXISTS idx_documents_author ON documents(author_did);
117117+CREATE INDEX IF NOT EXISTS idx_documents_site ON documents(site);
118118+CREATE INDEX IF NOT EXISTS idx_documents_canonical ON documents(canonical_url);
119119+CREATE INDEX IF NOT EXISTS idx_documents_published ON documents(published_at DESC);
120120+121121+-- +goose Down
122122+DROP TABLE IF EXISTS documents;
123123+DROP TABLE IF EXISTS publications;
124124+DROP TABLE IF EXISTS moderation_actions;
125125+DROP TABLE IF EXISTS moderation_reports;
126126+DROP TABLE IF EXISTS content_labels;
127127+DROP TABLE IF EXISTS mutes;
128128+DROP TABLE IF EXISTS blocks;
129129+DROP TABLE IF EXISTS collection_items;
130130+DROP TABLE IF EXISTS collections;
131131+DROP TABLE IF EXISTS cursors;
···11+-- +goose Up
22+ALTER TABLE sessions ADD COLUMN IF NOT EXISTS dpop_key TEXT;
33+44+ALTER TABLE annotations ADD COLUMN IF NOT EXISTS motivation TEXT;
55+ALTER TABLE annotations ADD COLUMN IF NOT EXISTS body_value TEXT;
66+ALTER TABLE annotations ADD COLUMN IF NOT EXISTS body_format TEXT DEFAULT 'text/plain';
77+ALTER TABLE annotations ADD COLUMN IF NOT EXISTS body_uri TEXT;
88+ALTER TABLE annotations ADD COLUMN IF NOT EXISTS target_source TEXT;
99+ALTER TABLE annotations ADD COLUMN IF NOT EXISTS target_hash TEXT;
1010+ALTER TABLE annotations ADD COLUMN IF NOT EXISTS target_title TEXT;
1111+ALTER TABLE annotations ADD COLUMN IF NOT EXISTS selector_json TEXT;
1212+ALTER TABLE annotations ADD COLUMN IF NOT EXISTS tags_json TEXT;
1313+ALTER TABLE annotations ADD COLUMN IF NOT EXISTS cid TEXT;
1414+1515+-- +goose StatementBegin
1616+DO $$
1717+BEGIN
1818+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'annotations' AND column_name = 'url') THEN
1919+ UPDATE annotations SET target_source = url WHERE target_source IS NULL AND url IS NOT NULL;
2020+ UPDATE annotations SET target_hash = url_hash WHERE target_hash IS NULL AND url_hash IS NOT NULL;
2121+ UPDATE annotations SET body_value = text WHERE body_value IS NULL AND text IS NOT NULL;
2222+ UPDATE annotations SET target_title = title WHERE target_title IS NULL AND title IS NOT NULL;
2323+ END IF;
2424+ UPDATE annotations SET motivation = 'commenting' WHERE motivation IS NULL;
2525+END $$;
2626+-- +goose StatementEnd
2727+2828+ALTER TABLE profiles ADD COLUMN IF NOT EXISTS website TEXT;
2929+ALTER TABLE profiles ADD COLUMN IF NOT EXISTS display_name TEXT;
3030+ALTER TABLE profiles ADD COLUMN IF NOT EXISTS avatar TEXT;
3131+3232+ALTER TABLE cursors ALTER COLUMN last_cursor TYPE BIGINT;
3333+3434+ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS uri TEXT;
3535+ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS cid TEXT;
3636+ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
3737+3838+ALTER TABLE preferences ADD COLUMN IF NOT EXISTS subscribed_labelers TEXT;
3939+ALTER TABLE preferences ADD COLUMN IF NOT EXISTS label_preferences TEXT;
4040+ALTER TABLE preferences ADD COLUMN IF NOT EXISTS disable_external_link_warning BOOLEAN;
4141+ALTER TABLE preferences ADD COLUMN IF NOT EXISTS enable_community_bookmarks BOOLEAN DEFAULT false;
4242+4343+-- +goose Down
+33
backend/internal/db/migrations/00004_views.sql
···11+-- +goose Up
22+-- +goose StatementBegin
33+CREATE OR REPLACE VIEW all_highlights AS
44+SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
55+FROM highlights
66+UNION ALL
77+SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
88+FROM notes WHERE motivation = 'highlighting';
99+-- +goose StatementEnd
1010+1111+-- +goose StatementBegin
1212+CREATE OR REPLACE VIEW all_annotations AS
1313+SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
1414+FROM annotations
1515+UNION ALL
1616+SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
1717+FROM notes WHERE motivation NOT IN ('highlighting', 'bookmarking');
1818+-- +goose StatementEnd
1919+2020+-- +goose StatementBegin
2121+CREATE OR REPLACE VIEW all_bookmarks AS
2222+SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
2323+FROM bookmarks
2424+UNION ALL
2525+SELECT uri, author_did, target_source AS source, target_hash AS source_hash, target_title AS title,
2626+ COALESCE(body_value, description) AS description, tags_json, created_at, indexed_at, cid
2727+FROM notes WHERE motivation = 'bookmarking';
2828+-- +goose StatementEnd
2929+3030+-- +goose Down
3131+DROP VIEW IF EXISTS all_bookmarks;
3232+DROP VIEW IF EXISTS all_annotations;
3333+DROP VIEW IF EXISTS all_highlights;
···11+-- +goose Up
22+CREATE TABLE IF NOT EXISTS document_embeddings (
33+ document_uri TEXT PRIMARY KEY,
44+ embedding TEXT NOT NULL,
55+ updated_at TIMESTAMP NOT NULL
66+);
77+88+CREATE TABLE IF NOT EXISTS annotation_embeddings (
99+ annotation_uri TEXT PRIMARY KEY,
1010+ author_did TEXT NOT NULL,
1111+ document_uri TEXT,
1212+ embedding TEXT NOT NULL,
1313+ updated_at TIMESTAMP NOT NULL
1414+);
1515+CREATE INDEX IF NOT EXISTS idx_ann_emb_author ON annotation_embeddings(author_did);
1616+CREATE INDEX IF NOT EXISTS idx_ann_emb_document ON annotation_embeddings(document_uri);
1717+1818+CREATE TABLE IF NOT EXISTS user_profiles (
1919+ author_did TEXT PRIMARY KEY,
2020+ embedding TEXT NOT NULL,
2121+ tag_affinities TEXT DEFAULT '{}',
2222+ annotation_count INTEGER NOT NULL DEFAULT 0,
2323+ updated_at TIMESTAMP NOT NULL
2424+);
2525+2626+-- +goose Down
2727+DROP TABLE IF EXISTS user_profiles;
2828+DROP TABLE IF EXISTS annotation_embeddings;
2929+DROP TABLE IF EXISTS document_embeddings;
+20-20
backend/internal/db/queries_annotations.go
···2828 var a Annotation
2929 err := db.QueryRow(`
3030 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
3131- FROM annotations
3131+ FROM all_annotations
3232 WHERE uri = $1
3333 `, uri).Scan(&a.URI, &a.AuthorDID, &a.Motivation, &a.BodyValue, &a.BodyFormat, &a.BodyURI, &a.TargetSource, &a.TargetHash, &a.TargetTitle, &a.SelectorJSON, &a.TagsJSON, &a.CreatedAt, &a.IndexedAt, &a.CID)
3434 if err != nil {
···4040func (db *DB) GetAnnotationsByTargetHash(targetHash string, limit, offset int) ([]Annotation, error) {
4141 rows, err := db.Query(`
4242 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
4343- FROM annotations
4343+ FROM all_annotations
4444 WHERE target_hash = $1
4545 ORDER BY created_at DESC
4646 LIMIT $2 OFFSET $3
···5656func (db *DB) GetAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) {
5757 rows, err := db.Query(`
5858 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
5959- FROM annotations
5959+ FROM all_annotations
6060 WHERE author_did = $1
6161 ORDER BY created_at DESC
6262 LIMIT $2 OFFSET $3
···7272func (db *DB) GetMarginAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) {
7373 rows, err := db.Query(`
7474 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
7575- FROM annotations
7575+ FROM all_annotations
7676 WHERE author_did = $1 AND uri NOT LIKE '%network.cosmik%'
7777 ORDER BY created_at DESC
7878 LIMIT $2 OFFSET $3
···8888func (db *DB) GetSembleAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) {
8989 rows, err := db.Query(`
9090 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
9191- FROM annotations
9191+ FROM all_annotations
9292 WHERE author_did = $1 AND uri LIKE '%network.cosmik%'
9393 ORDER BY created_at DESC
9494 LIMIT $2 OFFSET $3
···104104func (db *DB) GetAnnotationsByMotivation(motivation string, limit, offset int) ([]Annotation, error) {
105105 rows, err := db.Query(`
106106 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
107107- FROM annotations
107107+ FROM all_annotations
108108 WHERE motivation = $1
109109 ORDER BY created_at DESC
110110 LIMIT $2 OFFSET $3
···120120func (db *DB) GetRecentAnnotations(limit, offset int) ([]Annotation, error) {
121121 rows, err := db.Query(`
122122 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
123123- FROM annotations
123123+ FROM all_annotations
124124 ORDER BY created_at DESC
125125 LIMIT $1 OFFSET $2
126126 `, limit, offset)
···139139 a.uri, a.author_did, a.motivation, a.body_value, a.body_format,
140140 a.body_uri, a.target_source, a.target_hash, a.target_title,
141141 a.selector_json, a.tags_json, a.created_at, a.indexed_at, a.cid
142142- FROM annotations a
142142+ FROM all_annotations a
143143 LEFT JOIN LATERAL (
144144 SELECT COUNT(*) as cnt FROM likes WHERE subject_uri = a.uri
145145 ) l ON true
···166166 a.uri, a.author_did, a.motivation, a.body_value, a.body_format,
167167 a.body_uri, a.target_source, a.target_hash, a.target_title,
168168 a.selector_json, a.tags_json, a.created_at, a.indexed_at, a.cid
169169- FROM annotations a
169169+ FROM all_annotations a
170170 WHERE a.created_at < $1 AND a.created_at > $2
171171 AND NOT EXISTS (SELECT 1 FROM likes WHERE subject_uri = a.uri)
172172 AND NOT EXISTS (SELECT 1 FROM replies WHERE root_uri = a.uri)
···184184func (db *DB) GetMarginAnnotations(limit, offset int) ([]Annotation, error) {
185185 rows, err := db.Query(`
186186 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
187187- FROM annotations
187187+ FROM all_annotations
188188 WHERE uri NOT LIKE '%network.cosmik%'
189189 ORDER BY created_at DESC
190190 LIMIT $1 OFFSET $2
···200200func (db *DB) GetSembleAnnotations(limit, offset int) ([]Annotation, error) {
201201 rows, err := db.Query(`
202202 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
203203- FROM annotations
203203+ FROM all_annotations
204204 WHERE uri LIKE '%network.cosmik%'
205205 ORDER BY created_at DESC
206206 LIMIT $1 OFFSET $2
···216216func (db *DB) GetAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) {
217217 rows, err := db.Query(`
218218 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
219219- FROM annotations
219219+ FROM all_annotations
220220 WHERE EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $1)
221221 ORDER BY created_at DESC
222222 LIMIT $2 OFFSET $3
···232232func (db *DB) GetMarginAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) {
233233 rows, err := db.Query(`
234234 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
235235- FROM annotations
235235+ FROM all_annotations
236236 WHERE EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $1) AND uri NOT LIKE '%network.cosmik%'
237237 ORDER BY created_at DESC
238238 LIMIT $2 OFFSET $3
···248248func (db *DB) GetSembleAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) {
249249 rows, err := db.Query(`
250250 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
251251- FROM annotations
251251+ FROM all_annotations
252252 WHERE EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $1) AND uri LIKE '%network.cosmik%'
253253 ORDER BY created_at DESC
254254 LIMIT $2 OFFSET $3
···278278func (db *DB) GetAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) {
279279 rows, err := db.Query(`
280280 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
281281- FROM annotations
281281+ FROM all_annotations
282282 WHERE author_did = $1 AND EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $2)
283283 ORDER BY created_at DESC
284284 LIMIT $3 OFFSET $4
···294294func (db *DB) GetMarginAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) {
295295 rows, err := db.Query(`
296296 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
297297- FROM annotations
297297+ FROM all_annotations
298298 WHERE author_did = $1 AND EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $2) AND uri NOT LIKE '%network.cosmik%'
299299 ORDER BY created_at DESC
300300 LIMIT $3 OFFSET $4
···310310func (db *DB) GetSembleAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) {
311311 rows, err := db.Query(`
312312 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
313313- FROM annotations
313313+ FROM all_annotations
314314 WHERE author_did = $1 AND EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $2) AND uri LIKE '%network.cosmik%'
315315 ORDER BY created_at DESC
316316 LIMIT $3 OFFSET $4
···326326func (db *DB) GetAnnotationsByAuthorAndTargetHash(authorDID, targetHash string, limit, offset int) ([]Annotation, error) {
327327 rows, err := db.Query(`
328328 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
329329- FROM annotations
329329+ FROM all_annotations
330330 WHERE author_did = $1 AND target_hash = $2
331331 ORDER BY created_at DESC
332332 LIMIT $3 OFFSET $4
···346346347347 query := `
348348 SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
349349- FROM annotations
349349+ FROM all_annotations
350350 WHERE uri = ANY($1)
351351 `
352352···361361362362func (db *DB) GetAnnotationURIs(authorDID string) ([]string, error) {
363363 rows, err := db.Query(`
364364- SELECT uri FROM annotations WHERE author_did = $1
364364+ SELECT uri FROM all_annotations WHERE author_did = $1
365365 `, authorDID)
366366 if err != nil {
367367 return nil, err
+18-18
backend/internal/db/queries_bookmarks.go
···2424 var b Bookmark
2525 err := db.QueryRow(`
2626 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
2727- FROM bookmarks
2727+ FROM all_bookmarks
2828 WHERE uri = $1
2929 `, uri).Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID)
3030 if err != nil {
···3636func (db *DB) GetRecentBookmarks(limit, offset int) ([]Bookmark, error) {
3737 rows, err := db.Query(`
3838 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
3939- FROM bookmarks
3939+ FROM all_bookmarks
4040 ORDER BY created_at DESC
4141 LIMIT $1 OFFSET $2
4242 `, limit, offset)
···5454 SELECT
5555 b.uri, b.author_did, b.source, b.source_hash, b.title,
5656 b.description, b.tags_json, b.created_at, b.indexed_at, b.cid
5757- FROM bookmarks b
5757+ FROM all_bookmarks b
5858 LEFT JOIN LATERAL (
5959 SELECT COUNT(*) as cnt FROM likes WHERE subject_uri = b.uri
6060 ) l ON true
···8080 SELECT
8181 b.uri, b.author_did, b.source, b.source_hash, b.title,
8282 b.description, b.tags_json, b.created_at, b.indexed_at, b.cid
8383- FROM bookmarks b
8383+ FROM all_bookmarks b
8484 WHERE b.created_at < $1 AND b.created_at > $2
8585 AND NOT EXISTS (SELECT 1 FROM likes WHERE subject_uri = b.uri)
8686 AND NOT EXISTS (SELECT 1 FROM replies WHERE root_uri = b.uri)
···9898func (db *DB) GetMarginBookmarks(limit, offset int) ([]Bookmark, error) {
9999 rows, err := db.Query(`
100100 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
101101- FROM bookmarks
101101+ FROM all_bookmarks
102102 WHERE uri NOT LIKE '%network.cosmik%'
103103 ORDER BY created_at DESC
104104 LIMIT $1 OFFSET $2
···114114func (db *DB) GetSembleBookmarks(limit, offset int) ([]Bookmark, error) {
115115 rows, err := db.Query(`
116116 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
117117- FROM bookmarks
117117+ FROM all_bookmarks
118118 WHERE uri LIKE '%network.cosmik%'
119119 ORDER BY created_at DESC
120120 LIMIT $1 OFFSET $2
···130130func (db *DB) GetBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) {
131131 rows, err := db.Query(`
132132 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
133133- FROM bookmarks
133133+ FROM all_bookmarks
134134 WHERE EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $1)
135135 ORDER BY created_at DESC
136136 LIMIT $2 OFFSET $3
···146146func (db *DB) GetMarginBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) {
147147 rows, err := db.Query(`
148148 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
149149- FROM bookmarks
149149+ FROM all_bookmarks
150150 WHERE EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $1) AND uri NOT LIKE '%network.cosmik%'
151151 ORDER BY created_at DESC
152152 LIMIT $2 OFFSET $3
···162162func (db *DB) GetSembleBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) {
163163 rows, err := db.Query(`
164164 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
165165- FROM bookmarks
165165+ FROM all_bookmarks
166166 WHERE EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $1) AND uri LIKE '%network.cosmik%'
167167 ORDER BY created_at DESC
168168 LIMIT $2 OFFSET $3
···178178func (db *DB) GetBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) {
179179 rows, err := db.Query(`
180180 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
181181- FROM bookmarks
181181+ FROM all_bookmarks
182182 WHERE author_did = $1 AND EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $2)
183183 ORDER BY created_at DESC
184184 LIMIT $3 OFFSET $4
···194194func (db *DB) GetMarginBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) {
195195 rows, err := db.Query(`
196196 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
197197- FROM bookmarks
197197+ FROM all_bookmarks
198198 WHERE author_did = $1 AND EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $2) AND uri NOT LIKE '%network.cosmik%'
199199 ORDER BY created_at DESC
200200 LIMIT $3 OFFSET $4
···210210func (db *DB) GetSembleBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) {
211211 rows, err := db.Query(`
212212 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
213213- FROM bookmarks
213213+ FROM all_bookmarks
214214 WHERE author_did = $1 AND EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $2) AND uri LIKE '%network.cosmik%'
215215 ORDER BY created_at DESC
216216 LIMIT $3 OFFSET $4
···226226func (db *DB) GetBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) {
227227 rows, err := db.Query(`
228228 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
229229- FROM bookmarks
229229+ FROM all_bookmarks
230230 WHERE author_did = $1
231231 ORDER BY created_at DESC
232232 LIMIT $2 OFFSET $3
···242242func (db *DB) GetMarginBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) {
243243 rows, err := db.Query(`
244244 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
245245- FROM bookmarks
245245+ FROM all_bookmarks
246246 WHERE author_did = $1 AND uri NOT LIKE '%network.cosmik%'
247247 ORDER BY created_at DESC
248248 LIMIT $2 OFFSET $3
···258258func (db *DB) GetSembleBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) {
259259 rows, err := db.Query(`
260260 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
261261- FROM bookmarks
261261+ FROM all_bookmarks
262262 WHERE author_did = $1 AND uri LIKE '%network.cosmik%'
263263 ORDER BY created_at DESC
264264 LIMIT $2 OFFSET $3
···292292293293 rows, err := db.Query(`
294294 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
295295- FROM bookmarks
295295+ FROM all_bookmarks
296296 WHERE uri = ANY($1)
297297 `, pqStringArray(uris))
298298 if err != nil {
···305305306306func (db *DB) GetBookmarkURIs(authorDID string) ([]string, error) {
307307 rows, err := db.Query(`
308308- SELECT uri FROM bookmarks WHERE author_did = $1
308308+ SELECT uri FROM all_bookmarks WHERE author_did = $1
309309 `, authorDID)
310310 if err != nil {
311311 return nil, err
···326326func (db *DB) GetBookmarksByTargetHash(targetHash string, limit, offset int) ([]Bookmark, error) {
327327 rows, err := db.Query(`
328328 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
329329- FROM bookmarks
329329+ FROM all_bookmarks
330330 WHERE source_hash = $1
331331 ORDER BY created_at DESC
332332 LIMIT $2 OFFSET $3
+19-19
backend/internal/db/queries_highlights.go
···2525 var h Highlight
2626 err := db.QueryRow(`
2727 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
2828- FROM highlights
2828+ FROM all_highlights
2929 WHERE uri = $1
3030 `, uri).Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID)
3131 if err != nil {
···3737func (db *DB) GetRecentHighlights(limit, offset int) ([]Highlight, error) {
3838 rows, err := db.Query(`
3939 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
4040- FROM highlights
4040+ FROM all_highlights
4141 ORDER BY created_at DESC
4242 LIMIT $1 OFFSET $2
4343 `, limit, offset)
···5555 SELECT
5656 h.uri, h.author_did, h.target_source, h.target_hash, h.target_title,
5757 h.selector_json, h.color, h.tags_json, h.created_at, h.indexed_at, h.cid
5858- FROM highlights h
5858+ FROM all_highlights h
5959 LEFT JOIN LATERAL (
6060 SELECT COUNT(*) as cnt FROM likes WHERE subject_uri = h.uri
6161 ) l ON true
···8181 SELECT
8282 h.uri, h.author_did, h.target_source, h.target_hash, h.target_title,
8383 h.selector_json, h.color, h.tags_json, h.created_at, h.indexed_at, h.cid
8484- FROM highlights h
8484+ FROM all_highlights h
8585 WHERE h.created_at < $1 AND h.created_at > $2
8686 AND NOT EXISTS (SELECT 1 FROM likes WHERE subject_uri = h.uri)
8787 AND NOT EXISTS (SELECT 1 FROM replies WHERE root_uri = h.uri)
···9999func (db *DB) GetMarginHighlights(limit, offset int) ([]Highlight, error) {
100100 rows, err := db.Query(`
101101 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
102102- FROM highlights
102102+ FROM all_highlights
103103 WHERE uri NOT LIKE '%network.cosmik%'
104104 ORDER BY created_at DESC
105105 LIMIT $1 OFFSET $2
···115115func (db *DB) GetSembleHighlights(limit, offset int) ([]Highlight, error) {
116116 rows, err := db.Query(`
117117 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
118118- FROM highlights
118118+ FROM all_highlights
119119 WHERE uri LIKE '%network.cosmik%'
120120 ORDER BY created_at DESC
121121 LIMIT $1 OFFSET $2
···131131func (db *DB) GetHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) {
132132 rows, err := db.Query(`
133133 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
134134- FROM highlights
134134+ FROM all_highlights
135135 WHERE EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $1)
136136 ORDER BY created_at DESC
137137 LIMIT $2 OFFSET $3
···147147func (db *DB) GetMarginHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) {
148148 rows, err := db.Query(`
149149 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
150150- FROM highlights
150150+ FROM all_highlights
151151 WHERE EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $1) AND uri NOT LIKE '%network.cosmik%'
152152 ORDER BY created_at DESC
153153 LIMIT $2 OFFSET $3
···163163func (db *DB) GetSembleHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) {
164164 rows, err := db.Query(`
165165 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
166166- FROM highlights
166166+ FROM all_highlights
167167 WHERE EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $1) AND uri LIKE '%network.cosmik%'
168168 ORDER BY created_at DESC
169169 LIMIT $2 OFFSET $3
···179179func (db *DB) GetHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) {
180180 rows, err := db.Query(`
181181 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
182182- FROM highlights
182182+ FROM all_highlights
183183 WHERE author_did = $1 AND EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $2)
184184 ORDER BY created_at DESC
185185 LIMIT $3 OFFSET $4
···195195func (db *DB) GetMarginHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) {
196196 rows, err := db.Query(`
197197 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
198198- FROM highlights
198198+ FROM all_highlights
199199 WHERE author_did = $1 AND EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $2) AND uri NOT LIKE '%network.cosmik%'
200200 ORDER BY created_at DESC
201201 LIMIT $3 OFFSET $4
···211211func (db *DB) GetSembleHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) {
212212 rows, err := db.Query(`
213213 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
214214- FROM highlights
214214+ FROM all_highlights
215215 WHERE author_did = $1 AND EXISTS(SELECT 1 FROM jsonb_array_elements_text(tags_json::jsonb) elem WHERE lower(elem) = $2) AND uri LIKE '%network.cosmik%'
216216 ORDER BY created_at DESC
217217 LIMIT $3 OFFSET $4
···227227func (db *DB) GetHighlightsByTargetHash(targetHash string, limit, offset int) ([]Highlight, error) {
228228 rows, err := db.Query(`
229229 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
230230- FROM highlights
230230+ FROM all_highlights
231231 WHERE target_hash = $1
232232 ORDER BY created_at DESC
233233 LIMIT $2 OFFSET $3
···243243func (db *DB) GetHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) {
244244 rows, err := db.Query(`
245245 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
246246- FROM highlights
246246+ FROM all_highlights
247247 WHERE author_did = $1
248248 ORDER BY created_at DESC
249249 LIMIT $2 OFFSET $3
···259259func (db *DB) GetMarginHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) {
260260 rows, err := db.Query(`
261261 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
262262- FROM highlights
262262+ FROM all_highlights
263263 WHERE author_did = $1 AND uri NOT LIKE '%network.cosmik%'
264264 ORDER BY created_at DESC
265265 LIMIT $2 OFFSET $3
···275275func (db *DB) GetSembleHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) {
276276 rows, err := db.Query(`
277277 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
278278- FROM highlights
278278+ FROM all_highlights
279279 WHERE author_did = $1 AND uri LIKE '%network.cosmik%'
280280 ORDER BY created_at DESC
281281 LIMIT $2 OFFSET $3
···291291func (db *DB) GetHighlightsByAuthorAndTargetHash(authorDID, targetHash string, limit, offset int) ([]Highlight, error) {
292292 rows, err := db.Query(`
293293 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
294294- FROM highlights
294294+ FROM all_highlights
295295 WHERE author_did = $1 AND target_hash = $2
296296 ORDER BY created_at DESC
297297 LIMIT $3 OFFSET $4
···325325326326 rows, err := db.Query(`
327327 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
328328- FROM highlights
328328+ FROM all_highlights
329329 WHERE uri = ANY($1)
330330 `, pqStringArray(uris))
331331 if err != nil {
···338338339339func (db *DB) GetHighlightURIs(authorDID string) ([]string, error) {
340340 rows, err := db.Query(`
341341- SELECT uri FROM highlights WHERE author_did = $1
341341+ SELECT uri FROM all_highlights WHERE author_did = $1
342342 `, authorDID)
343343 if err != nil {
344344 return nil, err
···11111212 <div class="prose prose-surface dark:prose-invert max-w-none">
1313 <h1 class="font-display font-bold text-3xl mb-2 text-surface-900 dark:text-white">Privacy Policy</h1>
1414- <p class="text-surface-500 dark:text-surface-400 mb-8">Last updated: March 4, 2026</p>
1414+ <p class="text-surface-500 dark:text-surface-400 mb-8">Last updated: April 14, 2026</p>
15151616 <section class="mb-8">
1717 <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Overview</h2>
1818 <p class="text-surface-700 dark:text-surface-300 leading-relaxed">
1919- Margin ("we", "our", or "us") is a web annotation tool that lets you highlight, annotate, and bookmark any webpage. Your data is stored on the decentralized AT Protocol network, giving you ownership and control over your content.
1919+ Margin is a web annotation tool built on the AT Protocol, operated by <strong>Padding Labs LLC</strong> ("we", "our", or "us"). It lets you highlight, take notes, and bookmark any webpage. Because Margin is built on the AT Protocol, your notes are stored as records in your Personal Data Server (PDS) — you own your content and can take it with you. You must be at least 13 years old to use Margin.
2020 </p>
2121 </section>
22222323 <section class="mb-8">
2424 <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Data We Collect</h2>
2525+2526 <h3 class="text-lg font-semibold text-surface-900 dark:text-white mb-2">Account Information</h3>
2627 <p class="text-surface-700 dark:text-surface-300 mb-4">
2727- When you log in with your Bluesky/AT Protocol account, we access your:
2828+ When you log in with your AT Protocol account (e.g. Bluesky), we access your:
2829 </p>
2930 <ul class="list-disc pl-5 mb-4 text-surface-700 dark:text-surface-300 space-y-1">
3031 <li>Decentralized Identifier (DID)</li>
3132 <li>Handle (username)</li>
3232- <li>Display name and avatar (for showing your profile)</li>
3333+ <li>Display name, avatar, bio, and website (for showing your profile)</li>
3334 </ul>
34353535- <h3 class="text-lg font-semibold text-surface-900 dark:text-white mb-2">Annotations & Content</h3>
3636- <p class="text-surface-700 dark:text-surface-300 mb-4">When you use Margin, we store:</p>
3636+ <h3 class="text-lg font-semibold text-surface-900 dark:text-white mb-2">Notes & Content</h3>
3737+ <p class="text-surface-700 dark:text-surface-300 mb-4">When you create a note, we store:</p>
3738 <ul class="list-disc pl-5 mb-4 text-surface-700 dark:text-surface-300 space-y-1">
3838- <li>URLs of pages you annotate</li>
3939- <li>Text you highlight or select</li>
4040- <li>Annotations and comments you create</li>
4141- <li>Bookmarks you save</li>
4242- <li>Collections you organize content into</li>
3939+ <li>The full URL of the page you noted on</li>
4040+ <li>The page title at the time of the note</li>
4141+ <li>The text you selected (up to 5,000 characters)</li>
4242+ <li>Surrounding context around your selection (up to 500 characters before and after), used to re-locate the note on the page</li>
4343+ <li>Any text you write as part of the note</li>
4444+ <li>Tags you apply</li>
4545+ <li>Highlight color (if set)</li>
4646+ <li>Replies and likes on notes</li>
4747+ <li>Collections you create, and which notes belong to them</li>
4848+ <li>An edit history of any changes you make to your notes</li>
4349 </ul>
44504545- <h3 class="text-lg font-semibold text-surface-900 dark:text-white mb-2">Discover & Recommendations</h3>
5151+ <h3 class="text-lg font-semibold text-surface-900 dark:text-white mb-2">Overlay Lookups (Browser Extension)</h3>
5252+ <p class="text-surface-700 dark:text-surface-300 mb-4">
5353+ When you browse the web with the extension enabled, it checks each page for existing notes. For these lookups, the extension computes a SHA-256 hash of the page URL locally and sends only that hash to our server — <strong>the raw URL is not sent for lookups.</strong> A hash cannot be reversed to recover the original URL. You can disable overlay lookups entirely by turning off page overlays in the extension settings.
5454+ </p>
4655 <p class="text-surface-700 dark:text-surface-300 mb-4">
4747- To power the Discover page and personalized recommendations, we generate mathematical representations (embeddings) of:
5656+ Note: when you <em>create</em> a note, the full URL is included as part of the record stored on your PDS.
5757+ </p>
5858+5959+ <h3 class="text-lg font-semibold text-surface-900 dark:text-white mb-2">Recommendations</h3>
6060+ <p class="text-surface-700 dark:text-surface-300 mb-4">
6161+ To power the Discover page, we generate vector embeddings of your notes and public documents on the AT Protocol network. These embeddings are used to build an interest profile and surface relevant content. Your interest profile is stored on our server and is not shared with other users.
6262+ </p>
6363+ <p class="text-surface-700 dark:text-surface-300 mb-4">
6464+ Embeddings are generated via the <strong>OpenAI</strong> API. The text content of your notes and public AT Protocol documents is sent to OpenAI for this purpose. OpenAI does not use API inputs to train their models — see their <a href="https://openai.com/policies/usage-policies/" class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 hover:underline" target="_blank" rel="noopener noreferrer">API data usage policy</a>. Recommendations are only generated if you have used Margin to annotate content.
4865 </p>
4949- <ul class="list-disc pl-5 mb-4 text-surface-700 dark:text-surface-300 space-y-1">
5050- <li>Your annotations, highlights, and their associated tags</li>
5151- <li>Publicly published documents from the AT Protocol network</li>
5252- </ul>
6666+6767+ <h3 class="text-lg font-semibold text-surface-900 dark:text-white mb-2">Analytics</h3>
5368 <p class="text-surface-700 dark:text-surface-300 mb-4">
5454- These embeddings are used to build an interest profile that helps us suggest relevant content. Your interest profile is stored on our server and is not shared with other users.
6969+ We use <strong>PostHog</strong> for product analytics to understand how Margin is used and improve the product. Analytics events — such as page views, note creation, and feature interactions — are collected on the Margin website and server. These events may include the URL of the page being noted on, your browser type, and extension version. PostHog may set cookies for this purpose. We do not use analytics to track you across other websites or share analytics data with advertisers. See PostHog's <a href="https://posthog.com/privacy" class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 hover:underline" target="_blank" rel="noopener noreferrer">privacy policy</a>.
5570 </p>
56715772 <h3 class="text-lg font-semibold text-surface-900 dark:text-white mb-2">Authentication</h3>
5873 <p class="text-surface-700 dark:text-surface-300 mb-4">
5959- We store OAuth session tokens locally in your browser to keep you logged in. These tokens are used solely for authenticating API requests.
7474+ We store your OAuth session tokens (access token, refresh token, and DPoP proof key) in our database to keep you logged in. These are used solely for authenticating requests to the AT Protocol on your behalf and expire when your session ends.
6075 </p>
6176 </section>
62776378 <section class="mb-8">
6479 <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">How We Use Your Data</h2>
6565- <p class="text-surface-700 dark:text-surface-300 mb-4">Your data is used exclusively to:</p>
8080+ <p class="text-surface-700 dark:text-surface-300 mb-4">Your data is used to:</p>
6681 <ul class="list-disc pl-5 mb-4 text-surface-700 dark:text-surface-300 space-y-1">
6767- <li>Display your annotations on webpages</li>
8282+ <li>Display your notes on webpages via the browser extension</li>
6883 <li>Sync your content across devices</li>
6969- <li>Show your public annotations to other users</li>
8484+ <li>Show your public notes to other users on the same page</li>
7085 <li>Enable social features like replies and likes</li>
7186 <li>Generate personalized content recommendations on the Discover page</li>
8787+ <li>Understand product usage and improve the service</li>
7288 </ul>
7373-7474- <h3 class="text-lg font-semibold text-surface-900 dark:text-white mb-2">Third-Party Services</h3>
7575- <p class="text-surface-700 dark:text-surface-300 mb-4">
7676- We use <strong>OpenAI</strong> to generate text embeddings for powering recommendations. When generating embeddings, the text content of your annotations and public documents is sent to OpenAI's API. OpenAI processes this data according to their <a href="https://openai.com/policies/api-data-usage-policies" class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 hover:underline" target="_blank" rel="noopener noreferrer">API data usage policy</a>, which states that API inputs are not used to train their models. No other third-party services receive your data.
8989+ <p class="text-surface-700 dark:text-surface-300">
9090+ <strong>We do not sell your data.</strong> We do not share your data with third parties for advertising or marketing purposes.
7791 </p>
7892 </section>
79938094 <section class="mb-8">
8181- <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Data Storage</h2>
9595+ <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">The AT Protocol & Data Portability</h2>
8296 <p class="text-surface-700 dark:text-surface-300 mb-4">
8383- Your annotations are stored on the AT Protocol network through your Personal Data Server (PDS). This means:
9797+ Margin is built on the AT Protocol. Your notes, collections, and profile are stored as records in your Personal Data Server (PDS). This means:
8498 </p>
8599 <ul class="list-disc pl-5 mb-4 text-surface-700 dark:text-surface-300 space-y-1">
8686- <li>You own your data</li>
8787- <li>You can export or delete it at any time</li>
8888- <li>Your data is portable across AT Protocol services</li>
100100+ <li>You own your content</li>
101101+ <li>You can export your full data repository from your PDS at any time</li>
102102+ <li>Your data is portable — you can use it with other AT Protocol-compatible services</li>
103103+ <li>You can migrate your account to a different PDS without losing your content</li>
89104 </ul>
90105 <p class="text-surface-700 dark:text-surface-300 mb-4">
9191- We also maintain a local index of annotations to provide faster search and discovery features.
106106+ We also maintain a server-side index of public notes to power features like search, discovery, and the extension overlay. This index is derived from the public AT Protocol firehose and is used solely to operate the service.
92107 </p>
9310894109 <h3 class="text-lg font-semibold text-surface-900 dark:text-white mb-2">Margin PDS (margin.cafe)</h3>
95110 <p class="text-surface-700 dark:text-surface-300 mb-4">
9696- We operate a Personal Data Server at <strong>margin.cafe</strong>. If you create an account on this PDS, we additionally store:
111111+ We operate an optional Personal Data Server at <strong>margin.cafe</strong>. If you create an account there, we additionally store:
97112 </p>
98113 <ul class="list-disc pl-5 mb-4 text-surface-700 dark:text-surface-300 space-y-1">
9999- <li>Your account credentials (password hashed, never stored in plain text)</li>
114114+ <li>Your account credentials (password is hashed and never stored in plain text)</li>
100115 <li>Your email address (for account recovery)</li>
101116 <li>All AT Protocol data repositories you create on this server</li>
102117 </ul>
103118 <p class="text-surface-700 dark:text-surface-300">
104104- You can migrate your account and data to a different PDS at any time using standard AT Protocol account migration. Using the margin.cafe PDS is entirely optional as you can use Margin with any AT Protocol PDS.
119119+ Using margin.cafe is entirely optional — Margin works with any AT Protocol PDS. You can migrate your account to a different PDS at any time using standard AT Protocol account migration.
105120 </p>
106121 </section>
107122108123 <section class="mb-8">
109109- <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Data Sharing</h2>
124124+ <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Public Content</h2>
110125 <p class="text-surface-700 dark:text-surface-300 mb-4">
111111- <strong>We do not sell your data.</strong> We do not share your data with third parties for advertising or marketing purposes.
126126+ Notes you create are public by default on the AT Protocol network. This means they may be visible to:
112127 </p>
113113- <p class="text-surface-700 dark:text-surface-300 mb-4">Your public annotations may be visible to:</p>
114128 <ul class="list-disc pl-5 mb-4 text-surface-700 dark:text-surface-300 space-y-1">
115129 <li>Other Margin users viewing the same webpage</li>
116116- <li>Anyone on the AT Protocol network (for public content)</li>
130130+ <li>Anyone on the AT Protocol network who subscribes to public records</li>
117131 </ul>
118132 </section>
119133120134 <section class="mb-8">
121135 <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Browser Extension Permissions</h2>
122122- <p class="text-surface-700 dark:text-surface-300 mb-4">The Margin browser extension requires certain permissions:</p>
136136+ <p class="text-surface-700 dark:text-surface-300 mb-4">The Margin browser extension requires the following permissions:</p>
123137 <ul class="list-disc pl-5 mb-4 text-surface-700 dark:text-surface-300 space-y-1">
124124- <li>
125125- <strong>All URLs:</strong> To display and create annotations on any webpage. When checking a page for annotations, the URL is sent to our server. <strong>We do not store these requests, the URLs are redacted, and we do not link your identity to the URLs you visit.</strong> You can disable sending URLs to Margin by turning off page overlays in the extension settings.
126126- </li>
138138+ <li><strong>All URLs:</strong> To display and create notes on any webpage. Overlay lookups use a local SHA-256 hash of the URL — the raw URL is only sent when you actively create a note.</li>
127139 <li><strong>Storage:</strong> To save your preferences and session locally</li>
128140 <li><strong>Cookies:</strong> To maintain your logged-in session</li>
129129- <li><strong>Tabs:</strong> To know which page you're viewing</li>
141141+ <li><strong>Tabs:</strong> To know which page you're currently viewing</li>
130142 </ul>
131143 </section>
132144···134146 <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Your Rights</h2>
135147 <p class="text-surface-700 dark:text-surface-300 mb-4">You can:</p>
136148 <ul class="list-disc pl-5 mb-4 text-surface-700 dark:text-surface-300 space-y-1">
137137- <li>Delete any annotation, highlight, or bookmark you've created</li>
149149+ <li>Delete any note or reply you've created</li>
138150 <li>Delete your collections</li>
139139- <li>Export your data from your PDS</li>
140140- <li>Revoke the extension's access at any time</li>
151151+ <li>Export your full data repository from your PDS</li>
152152+ <li>Revoke Margin's OAuth access to your account at any time</li>
153153+ <li>Request deletion of your margin.cafe account by contacting us — deletion of AT Protocol data on your PDS is governed by your PDS provider</li>
141154 </ul>
155155+ <p class="text-surface-700 dark:text-surface-300">
156156+ To exercise any of these rights, email us at <a href="mailto:hello@margin.at" class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 hover:underline">hello@margin.at</a>.
157157+ </p>
142158 </section>
143159144160 <section class="mb-8">
+48-10
web/src/pages/terms.astro
···11111212 <div class="prose prose-surface dark:prose-invert max-w-none">
1313 <h1 class="font-display font-bold text-3xl mb-2 text-surface-900 dark:text-white">Terms of Service</h1>
1414- <p class="text-surface-500 dark:text-surface-400 mb-8">Last updated: January 17, 2026</p>
1414+ <p class="text-surface-500 dark:text-surface-400 mb-8">Last updated: April 14, 2026</p>
15151616 <section class="mb-8">
1717 <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Overview</h2>
1818 <p class="text-surface-700 dark:text-surface-300 leading-relaxed">
1919- Margin is an open-source project. By using our service, you agree to these terms ("Terms"). If you do not agree to these Terms, please do not use the Service.
1919+ Margin is a web annotation tool built on the AT Protocol, operated by Padding Labs LLC ("we", "our", or "us"). By using Margin (the "Service"), you agree to these Terms. If you do not agree, please do not use the Service. You must be at least 13 years old to use Margin.
2020 </p>
2121 </section>
22222323 <section class="mb-8">
2424 <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Open Source</h2>
2525 <p class="text-surface-700 dark:text-surface-300 leading-relaxed">
2626- Margin is open source software. The code is available publicly and is provided "as is", without warranty of any kind, express or implied.
2626+ Margin is open source software. The source code is publicly available and provided "as is", without warranty of any kind, express or implied. These Terms govern your use of the hosted service at margin.at, not the source code itself.
2727+ </p>
2828+ </section>
2929+3030+ <section class="mb-8">
3131+ <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Your Content</h2>
3232+ <p class="text-surface-700 dark:text-surface-300 mb-4">
3333+ You retain full ownership of all notes and other content you create ("Your Content"). By using the Service, you grant Padding Labs LLC a limited, non-exclusive, royalty-free license to store, index, display, and sync Your Content solely as needed to operate and improve the Service.
3434+ </p>
3535+ <p class="text-surface-700 dark:text-surface-300">
3636+ Because Margin is built on the AT Protocol, Your Content is stored as records on your Personal Data Server (PDS). It is portable and exportable at any time — we do not lock you in.
3737+ </p>
3838+ </section>
3939+4040+ <section class="mb-8">
4141+ <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Public Notes</h2>
4242+ <p class="text-surface-700 dark:text-surface-300 leading-relaxed">
4343+ Notes you create are public by default on the AT Protocol network and may be visible to other users and services that interact with the network. Do not include sensitive or private information in your notes if you do not want it to be publicly visible.
2744 </p>
2845 </section>
2946···3350 You are responsible for your use of the Service and for any content you provide, including compliance with applicable laws, rules, and regulations.
3451 </p>
3552 <p class="text-surface-700 dark:text-surface-300 mb-4">
3636- We reserve the right to remove any content that violates these terms, including but not limited to:
5353+ We reserve the right to remove content or suspend access for violations of these Terms, including but not limited to:
3754 </p>
3855 <ul class="list-disc pl-5 mb-4 text-surface-700 dark:text-surface-300 space-y-1">
3956 <li>Illegal content</li>
···4360 </section>
44614562 <section class="mb-8">
4646- <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Decentralized Nature</h2>
6363+ <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">The AT Protocol</h2>
4764 <p class="text-surface-700 dark:text-surface-300 leading-relaxed">
4848- Margin interacts with the AT Protocol network. We do not control the network itself or the data stored on your Personal Data Server (PDS). Please refer to the terms of your PDS provider for data storage policies.
6565+ Margin interacts with the AT Protocol network. We do not control the network itself, other services on the network, or the data stored on your Personal Data Server. Your PDS provider's own terms govern data stored there. Content published to the AT Protocol is public and may be indexed by other services beyond Margin's control.
4966 </p>
5067 </section>
51685269 <section class="mb-8">
5353- <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Disclaimer</h2>
5454- <p class="text-surface-700 dark:text-surface-300 leading-relaxed uppercase">
5555- THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE". WE DISCLAIM ALL CONDITIONS, REPRESENTATIONS AND WARRANTIES NOT EXPRESSLY SET OUT IN THESE TERMS.
7070+ <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Disclaimer of Warranties</h2>
7171+ <p class="text-surface-700 dark:text-surface-300 leading-relaxed">
7272+ The Service is provided "as is" and "as available" without warranties of any kind, either express or implied, including but not limited to implied warranties of merchantability, fitness for a particular purpose, or non-infringement. We do not warrant that the Service will be uninterrupted, error-free, or free of harmful components.
7373+ </p>
7474+ </section>
7575+7676+ <section class="mb-8">
7777+ <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Limitation of Liability</h2>
7878+ <p class="text-surface-700 dark:text-surface-300 leading-relaxed">
7979+ To the fullest extent permitted by law, Padding Labs LLC shall not be liable for any indirect, incidental, special, consequential, or punitive damages arising out of or related to your use of the Service. Because Margin is provided free of charge, our total aggregate liability to you shall not exceed $0.
8080+ </p>
8181+ </section>
8282+8383+ <section class="mb-8">
8484+ <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Governing Law</h2>
8585+ <p class="text-surface-700 dark:text-surface-300 leading-relaxed">
8686+ These Terms are governed by the laws of the State of Wyoming, without regard to its conflict of law provisions.
8787+ </p>
8888+ </section>
8989+9090+ <section class="mb-8">
9191+ <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Changes to These Terms</h2>
9292+ <p class="text-surface-700 dark:text-surface-300 leading-relaxed">
9393+ We may update these Terms from time to time. We will note the date of the last update at the top of this page. Continued use of the Service after changes constitutes acceptance of the updated Terms.
5694 </p>
5795 </section>
58965997 <section class="mb-8">
6098 <h2 class="text-xl font-bold text-surface-900 dark:text-white mb-4">Contact</h2>
6199 <p class="text-surface-700 dark:text-surface-300">
6262- For questions about these Terms, please contact us at <a href="mailto:hello@margin.at" class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 hover:underline">hello@margin.at</a>
100100+ For questions about these Terms, contact us at <a href="mailto:hello@margin.at" class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 hover:underline">hello@margin.at</a>
63101 </p>
64102 </section>
65103 </div>
+7
web/src/store/auth.ts
···11import { atom } from "nanostores";
22import { loadPreferences } from "./preferences";
33import type { UserProfile } from "../types";
44+import { analytics } from "../lib/analytics";
4556export const $user = atom<UserProfile | null>(null);
6778$user.subscribe((user) => {
89 if (user) {
910 loadPreferences();
1111+ analytics.identify(user.did, {
1212+ handle: user.handle,
1313+ displayName: user.displayName,
1414+ });
1015 }
1116});
12171318export function logout() {
1919+ analytics.capture("user_logged_out");
2020+ analytics.reset();
1421 $user.set(null);
1522 fetch("/auth/logout", { method: "POST" }).finally(() => {
1623 window.location.href = "/";