a tool for shared writing and social publishing

add bsky_profiles table and subscribe to records

+447 -29
+1
actions/getIdentityData.ts
··· 15 15 `*, 16 16 identities( 17 17 *, 18 + bsky_profiles(*), 18 19 subscribers_to_publications(*), 19 20 custom_domains(*), 20 21 home_leaflet:permission_tokens!identities_home_page_fkey(*, permission_token_rights(*)),
+14 -1
app/lish/subscribeToPublication.ts
··· 10 10 import { AtUri } from "@atproto/syntax"; 11 11 import { redirect } from "next/navigation"; 12 12 import { encodeActionToSearchParam } from "app/api/oauth/[route]/afterSignInActions"; 13 + import { Json } from "supabase/database.types"; 13 14 14 15 let leafletFeedURI = 15 16 "at://did:plc:btxrwcaeyodrap5mnjw2fvmz/app.bsky.feed.generator/subscribedPublications"; ··· 44 45 identity: credentialSession.did!, 45 46 }); 46 47 let bsky = new BskyAgent(credentialSession); 47 - let prefs = await bsky.app.bsky.actor.getPreferences(); 48 + let [prefs, profile] = await Promise.all([ 49 + bsky.app.bsky.actor.getPreferences(), 50 + bsky.app.bsky.actor.profile.get({ 51 + repo: credentialSession.did!, 52 + rkey: "self", 53 + }), 54 + ]); 55 + if (!identity.bsky_profiles && profile.value) { 56 + await supabaseServerClient.from("bsky_profiles").insert({ 57 + did: identity.atp_did, 58 + record: profile.value as Json, 59 + }); 60 + } 48 61 let savedFeeds = prefs.data.preferences.find( 49 62 (pref) => pref.$type === "app.bsky.actor.defs#savedFeedsPrefV2", 50 63 ) as AppBskyActorDefs.SavedFeedsPrefV2;
+50 -11
appview/index.ts
··· 11 11 } from "lexicons/api"; 12 12 import { AtUri } from "@atproto/syntax"; 13 13 import { writeFile, readFile } from "fs/promises"; 14 + import { createIdentity } from "actions/createIdentity"; 15 + import { supabaseServerClient } from "supabase/serverClient"; 16 + import postgres from "postgres"; 17 + import { drizzle } from "drizzle-orm/postgres-js"; 14 18 15 19 const cursorFile = process.env.CURSOR_FILE || "/cursor/cursor"; 16 20 ··· 23 27 try { 24 28 startCursor = parseInt((await readFile(cursorFile)).toString()); 25 29 } catch (e) {} 30 + 31 + const client = postgres(process.env.DB_URL!); 32 + const db = drizzle(client); 26 33 const runner = new MemoryRunner({ 27 34 startCursor, 28 35 setCursor: async (cursor) => { ··· 40 47 ids.PubLeafletDocument, 41 48 ids.PubLeafletPublication, 42 49 ids.PubLeafletGraphSubscription, 50 + ids.AppBskyActorProfile, 43 51 ], 44 52 handleEvent: async (evt) => { 45 53 if ( ··· 81 89 if (evt.event === "create" || evt.event === "update") { 82 90 let record = PubLeafletPublication.validateRecord(evt.record); 83 91 if (!record.success) return; 84 - await supabase.from("publications").upsert({ 92 + let { error } = await supabase.from("publications").upsert({ 85 93 uri: evt.uri.toString(), 86 94 identity_did: evt.did, 87 95 name: record.value.name, 88 96 record: record.value as Json, 89 97 }); 98 + 99 + if (error && error.code === "23503") { 100 + await createIdentity(db, { atp_did: evt.did }); 101 + await supabase.from("publications").upsert({ 102 + uri: evt.uri.toString(), 103 + identity_did: evt.did, 104 + name: record.value.name, 105 + record: record.value as Json, 106 + }); 107 + } 90 108 } 91 109 if (evt.event === "delete") { 92 110 await supabase ··· 95 113 .eq("uri", evt.uri.toString()); 96 114 } 97 115 } 98 - if (evt.collection === ids.PubLeafletPublication) { 116 + if (evt.collection === ids.PubLeafletGraphSubscription) { 99 117 if (evt.event === "create" || evt.event === "update") { 100 118 let record = PubLeafletGraphSubscription.validateRecord(evt.record); 101 119 if (!record.success) return; 102 - await supabase.from("publication_subscriptions").upsert({ 103 - uri: evt.uri.toString(), 104 - identity: evt.did, 105 - publication: record.value.publication, 106 - record: record.value as Json, 107 - }); 120 + let { error } = await supabase 121 + .from("publication_subscriptions") 122 + .upsert({ 123 + uri: evt.uri.toString(), 124 + identity: evt.did, 125 + publication: record.value.publication, 126 + record: record.value as Json, 127 + }); 128 + if (error && error.code === "23503") { 129 + await createIdentity(db, { atp_did: evt.did }); 130 + await supabase.from("publication_subscriptions").upsert({ 131 + uri: evt.uri.toString(), 132 + identity: evt.did, 133 + publication: record.value.publication, 134 + record: record.value as Json, 135 + }); 136 + } 108 137 } 109 138 if (evt.event === "delete") { 110 139 await supabase ··· 113 142 .eq("uri", evt.uri.toString()); 114 143 } 115 144 } 145 + if (evt.collection === ids.AppBskyActorProfile) { 146 + //only listen to updates because we should fetch it for the first time when they subscribe! 147 + if (evt.event === "update") { 148 + await supabaseServerClient 149 + .from("bsky_profiles") 150 + .update({ record: evt.record as Json }) 151 + .eq("did", evt.did); 152 + } 153 + } 116 154 }, 117 155 onError: (err) => { 118 156 console.error(err); ··· 120 158 }); 121 159 console.log("starting firehose consumer"); 122 160 firehose.start(); 123 - const cleanup = () => { 161 + const cleanup = async () => { 124 162 console.log("shutting down firehose..."); 125 - firehose.destroy(); 126 - runner.destroy(); 163 + await client.end(); 164 + await firehose.destroy(); 165 + await runner.destroy(); 127 166 process.exit(); 128 167 }; 129 168
+26 -13
drizzle/relations.ts
··· 1 1 import { relations } from "drizzle-orm/relations"; 2 - import { entities, facts, entity_sets, permission_tokens, identities, email_subscriptions_to_entity, email_auth_tokens, custom_domains, phone_rsvps_to_entity, custom_domain_routes, poll_votes_on_entity, subscribers_to_publications, publications, permission_token_on_homepage, documents, documents_in_publications, publication_domains, publication_subscriptions, leaflets_in_publications, permission_token_rights } from "./schema"; 2 + import { identities, bsky_profiles, entities, facts, entity_sets, permission_tokens, email_subscriptions_to_entity, email_auth_tokens, custom_domains, phone_rsvps_to_entity, custom_domain_routes, poll_votes_on_entity, subscribers_to_publications, publications, permission_token_on_homepage, documents, documents_in_publications, publication_domains, publication_subscriptions, leaflets_in_publications, permission_token_rights } from "./schema"; 3 + 4 + export const bsky_profilesRelations = relations(bsky_profiles, ({one}) => ({ 5 + identity: one(identities, { 6 + fields: [bsky_profiles.did], 7 + references: [identities.atp_did] 8 + }), 9 + })); 10 + 11 + export const identitiesRelations = relations(identities, ({one, many}) => ({ 12 + bsky_profiles: many(bsky_profiles), 13 + permission_token: one(permission_tokens, { 14 + fields: [identities.home_page], 15 + references: [permission_tokens.id] 16 + }), 17 + email_auth_tokens: many(email_auth_tokens), 18 + custom_domains: many(custom_domains), 19 + subscribers_to_publications: many(subscribers_to_publications), 20 + permission_token_on_homepages: many(permission_token_on_homepage), 21 + publication_domains: many(publication_domains), 22 + publication_subscriptions: many(publication_subscriptions), 23 + })); 3 24 4 25 export const factsRelations = relations(facts, ({one}) => ({ 5 26 entity: one(entities, { ··· 46 67 permission_token_on_homepages: many(permission_token_on_homepage), 47 68 leaflets_in_publications: many(leaflets_in_publications), 48 69 permission_token_rights: many(permission_token_rights), 49 - })); 50 - 51 - export const identitiesRelations = relations(identities, ({one, many}) => ({ 52 - permission_token: one(permission_tokens, { 53 - fields: [identities.home_page], 54 - references: [permission_tokens.id] 55 - }), 56 - email_auth_tokens: many(email_auth_tokens), 57 - custom_domains: many(custom_domains), 58 - subscribers_to_publications: many(subscribers_to_publications), 59 - permission_token_on_homepages: many(permission_token_on_homepage), 60 - publication_domains: many(publication_domains), 61 70 })); 62 71 63 72 export const email_subscriptions_to_entityRelations = relations(email_subscriptions_to_entity, ({one}) => ({ ··· 186 195 })); 187 196 188 197 export const publication_subscriptionsRelations = relations(publication_subscriptions, ({one}) => ({ 198 + identity: one(identities, { 199 + fields: [publication_subscriptions.identity], 200 + references: [identities.atp_did] 201 + }), 189 202 publication: one(publications, { 190 203 fields: [publication_subscriptions.publication], 191 204 references: [publications.uri]
+10 -4
drizzle/schema.ts
··· 1 - import { pgTable, pgEnum, text, jsonb, timestamp, foreignKey, uuid, bigint, boolean, unique, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" 1 + import { pgTable, pgEnum, text, jsonb, foreignKey, timestamp, uuid, bigint, boolean, unique, uniqueIndex, smallint, primaryKey } from "drizzle-orm/pg-core" 2 2 import { sql } from "drizzle-orm" 3 3 4 4 export const aal_level = pgEnum("aal_level", ['aal1', 'aal2', 'aal3']) ··· 22 22 export const oauth_session_store = pgTable("oauth_session_store", { 23 23 key: text("key").primaryKey().notNull(), 24 24 session: jsonb("session").notNull(), 25 + }); 26 + 27 + export const bsky_profiles = pgTable("bsky_profiles", { 28 + did: text("did").primaryKey().notNull().references(() => identities.atp_did, { onDelete: "cascade" } ), 29 + record: jsonb("record").notNull(), 30 + indexed_at: timestamp("indexed_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 25 31 }); 26 32 27 33 export const publications = pgTable("publications", { ··· 207 213 208 214 export const publication_subscriptions = pgTable("publication_subscriptions", { 209 215 publication: text("publication").notNull().references(() => publications.uri, { onDelete: "cascade" } ), 210 - identity: text("identity").notNull(), 216 + identity: text("identity").notNull().references(() => identities.atp_did, { onDelete: "cascade" } ), 211 217 created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 212 218 record: jsonb("record").notNull(), 213 219 uri: text("uri").notNull(), ··· 220 226 }); 221 227 222 228 export const leaflets_in_publications = pgTable("leaflets_in_publications", { 223 - publication: text("publication").notNull().references(() => publications.uri), 229 + publication: text("publication").notNull().references(() => publications.uri, { onDelete: "cascade" } ), 224 230 doc: text("doc").default('').references(() => documents.uri, { onDelete: "set null" } ), 225 - leaflet: uuid("leaflet").notNull().references(() => permission_tokens.id), 231 + leaflet: uuid("leaflet").notNull().references(() => permission_tokens.id, { onDelete: "cascade" } ), 226 232 description: text("description").default('').notNull(), 227 233 title: text("title").default('').notNull(), 228 234 },
+100
lexicons/api/index.ts
··· 27 27 import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' 28 28 import * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef' 29 29 import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' 30 + import * as AppBskyActorProfile from './types/app/bsky/actor/profile' 30 31 31 32 export * as PubLeafletDocument from './types/pub/leaflet/document' 32 33 export * as PubLeafletPublication from './types/pub/leaflet/publication' ··· 50 51 export * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' 51 52 export * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef' 52 53 export * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' 54 + export * as AppBskyActorProfile from './types/app/bsky/actor/profile' 53 55 54 56 export const PUB_LEAFLET_PAGES = { 55 57 LinearDocumentTextAlignLeft: 'pub.leaflet.pages.linearDocument#textAlignLeft', ··· 62 64 export class AtpBaseClient extends XrpcClient { 63 65 pub: PubNS 64 66 com: ComNS 67 + app: AppNS 65 68 66 69 constructor(options: FetchHandler | FetchHandlerOptions) { 67 70 super(options, schemas) 68 71 this.pub = new PubNS(this) 69 72 this.com = new ComNS(this) 73 + this.app = new AppNS(this) 70 74 } 71 75 72 76 /** @deprecated use `this` instead */ ··· 472 476 ) 473 477 } 474 478 } 479 + 480 + export class AppNS { 481 + _client: XrpcClient 482 + bsky: AppBskyNS 483 + 484 + constructor(client: XrpcClient) { 485 + this._client = client 486 + this.bsky = new AppBskyNS(client) 487 + } 488 + } 489 + 490 + export class AppBskyNS { 491 + _client: XrpcClient 492 + actor: AppBskyActorNS 493 + 494 + constructor(client: XrpcClient) { 495 + this._client = client 496 + this.actor = new AppBskyActorNS(client) 497 + } 498 + } 499 + 500 + export class AppBskyActorNS { 501 + _client: XrpcClient 502 + profile: ProfileRecord 503 + 504 + constructor(client: XrpcClient) { 505 + this._client = client 506 + this.profile = new ProfileRecord(client) 507 + } 508 + } 509 + 510 + export class ProfileRecord { 511 + _client: XrpcClient 512 + 513 + constructor(client: XrpcClient) { 514 + this._client = client 515 + } 516 + 517 + async list( 518 + params: OmitKey<ComAtprotoRepoListRecords.QueryParams, 'collection'>, 519 + ): Promise<{ 520 + cursor?: string 521 + records: { uri: string; value: AppBskyActorProfile.Record }[] 522 + }> { 523 + const res = await this._client.call('com.atproto.repo.listRecords', { 524 + collection: 'app.bsky.actor.profile', 525 + ...params, 526 + }) 527 + return res.data 528 + } 529 + 530 + async get( 531 + params: OmitKey<ComAtprotoRepoGetRecord.QueryParams, 'collection'>, 532 + ): Promise<{ uri: string; cid: string; value: AppBskyActorProfile.Record }> { 533 + const res = await this._client.call('com.atproto.repo.getRecord', { 534 + collection: 'app.bsky.actor.profile', 535 + ...params, 536 + }) 537 + return res.data 538 + } 539 + 540 + async create( 541 + params: OmitKey< 542 + ComAtprotoRepoCreateRecord.InputSchema, 543 + 'collection' | 'record' 544 + >, 545 + record: Un$Typed<AppBskyActorProfile.Record>, 546 + headers?: Record<string, string>, 547 + ): Promise<{ uri: string; cid: string }> { 548 + const collection = 'app.bsky.actor.profile' 549 + const res = await this._client.call( 550 + 'com.atproto.repo.createRecord', 551 + undefined, 552 + { 553 + collection, 554 + rkey: 'self', 555 + ...params, 556 + record: { ...record, $type: collection }, 557 + }, 558 + { encoding: 'application/json', headers }, 559 + ) 560 + return res.data 561 + } 562 + 563 + async delete( 564 + params: OmitKey<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>, 565 + headers?: Record<string, string>, 566 + ): Promise<void> { 567 + await this._client.call( 568 + 'com.atproto.repo.deleteRecord', 569 + undefined, 570 + { collection: 'app.bsky.actor.profile', ...params }, 571 + { headers }, 572 + ) 573 + } 574 + }
+60
lexicons/api/lexicons.ts
··· 1328 1328 }, 1329 1329 }, 1330 1330 }, 1331 + AppBskyActorProfile: { 1332 + lexicon: 1, 1333 + id: 'app.bsky.actor.profile', 1334 + defs: { 1335 + main: { 1336 + type: 'record', 1337 + description: 'A declaration of a Bluesky account profile.', 1338 + key: 'literal:self', 1339 + record: { 1340 + type: 'object', 1341 + properties: { 1342 + displayName: { 1343 + type: 'string', 1344 + maxGraphemes: 64, 1345 + maxLength: 640, 1346 + }, 1347 + description: { 1348 + type: 'string', 1349 + description: 'Free-form profile description text.', 1350 + maxGraphemes: 256, 1351 + maxLength: 2560, 1352 + }, 1353 + avatar: { 1354 + type: 'blob', 1355 + description: 1356 + "Small image to be displayed next to posts from account. AKA, 'profile picture'", 1357 + accept: ['image/png', 'image/jpeg'], 1358 + maxSize: 1000000, 1359 + }, 1360 + banner: { 1361 + type: 'blob', 1362 + description: 1363 + 'Larger horizontal image to display behind profile view.', 1364 + accept: ['image/png', 'image/jpeg'], 1365 + maxSize: 1000000, 1366 + }, 1367 + labels: { 1368 + type: 'union', 1369 + description: 1370 + 'Self-label values, specific to the Bluesky application, on the overall account.', 1371 + refs: ['lex:com.atproto.label.defs#selfLabels'], 1372 + }, 1373 + joinedViaStarterPack: { 1374 + type: 'ref', 1375 + ref: 'lex:com.atproto.repo.strongRef', 1376 + }, 1377 + pinnedPost: { 1378 + type: 'ref', 1379 + ref: 'lex:com.atproto.repo.strongRef', 1380 + }, 1381 + createdAt: { 1382 + type: 'string', 1383 + format: 'datetime', 1384 + }, 1385 + }, 1386 + }, 1387 + }, 1388 + }, 1389 + }, 1331 1390 } as const satisfies Record<string, LexiconDoc> 1332 1391 1333 1392 export const schemas = Object.values(schemaDict) satisfies LexiconDoc[] ··· 1384 1443 ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord', 1385 1444 ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', 1386 1445 ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', 1446 + AppBskyActorProfile: 'app.bsky.actor.profile', 1387 1447 } as const
+39
lexicons/api/types/app/bsky/actor/profile.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../../lexicons' 7 + import { $Typed, is$typed as _is$typed, OmitKey } from '../../../../util' 8 + import type * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' 9 + import type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef' 10 + 11 + const is$typed = _is$typed, 12 + validate = _validate 13 + const id = 'app.bsky.actor.profile' 14 + 15 + export interface Record { 16 + $type: 'app.bsky.actor.profile' 17 + displayName?: string 18 + /** Free-form profile description text. */ 19 + description?: string 20 + /** Small image to be displayed next to posts from account. AKA, 'profile picture' */ 21 + avatar?: BlobRef 22 + /** Larger horizontal image to display behind profile view. */ 23 + banner?: BlobRef 24 + labels?: $Typed<ComAtprotoLabelDefs.SelfLabels> | { $type: string } 25 + joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main 26 + pinnedPost?: ComAtprotoRepoStrongRef.Main 27 + createdAt?: string 28 + [k: string]: unknown 29 + } 30 + 31 + const hashRecord = 'main' 32 + 33 + export function isRecord<V>(v: V) { 34 + return is$typed(v, id, hashRecord) 35 + } 36 + 37 + export function validateRecord<V>(v: V) { 38 + return validate<Record & V>(v, id, hashRecord, true) 39 + }
+53
lexicons/app/bsky/actor/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.actor.profile", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A declaration of a Bluesky account profile.", 8 + "key": "literal:self", 9 + "record": { 10 + "type": "object", 11 + "properties": { 12 + "displayName": { 13 + "type": "string", 14 + "maxGraphemes": 64, 15 + "maxLength": 640 16 + }, 17 + "description": { 18 + "type": "string", 19 + "description": "Free-form profile description text.", 20 + "maxGraphemes": 256, 21 + "maxLength": 2560 22 + }, 23 + "avatar": { 24 + "type": "blob", 25 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'", 26 + "accept": ["image/png", "image/jpeg"], 27 + "maxSize": 1000000 28 + }, 29 + "banner": { 30 + "type": "blob", 31 + "description": "Larger horizontal image to display behind profile view.", 32 + "accept": ["image/png", "image/jpeg"], 33 + "maxSize": 1000000 34 + }, 35 + "labels": { 36 + "type": "union", 37 + "description": "Self-label values, specific to the Bluesky application, on the overall account.", 38 + "refs": ["com.atproto.label.defs#selfLabels"] 39 + }, 40 + "joinedViaStarterPack": { 41 + "type": "ref", 42 + "ref": "com.atproto.repo.strongRef" 43 + }, 44 + "pinnedPost": { 45 + "type": "ref", 46 + "ref": "com.atproto.repo.strongRef" 47 + }, 48 + "createdAt": { "type": "string", "format": "datetime" } 49 + } 50 + } 51 + } 52 + } 53 + }
+33
supabase/database.types.ts
··· 34 34 } 35 35 public: { 36 36 Tables: { 37 + bsky_profiles: { 38 + Row: { 39 + did: string 40 + indexed_at: string 41 + record: Json 42 + } 43 + Insert: { 44 + did: string 45 + indexed_at?: string 46 + record: Json 47 + } 48 + Update: { 49 + did?: string 50 + indexed_at?: string 51 + record?: Json 52 + } 53 + Relationships: [ 54 + { 55 + foreignKeyName: "bsky_profiles_did_fkey" 56 + columns: ["did"] 57 + isOneToOne: true 58 + referencedRelation: "identities" 59 + referencedColumns: ["atp_did"] 60 + }, 61 + ] 62 + } 37 63 custom_domain_routes: { 38 64 Row: { 39 65 created_at: string ··· 707 733 uri?: string 708 734 } 709 735 Relationships: [ 736 + { 737 + foreignKeyName: "publication_subscriptions_identity_fkey" 738 + columns: ["identity"] 739 + isOneToOne: false 740 + referencedRelation: "identities" 741 + referencedColumns: ["atp_did"] 742 + }, 710 743 { 711 744 foreignKeyName: "publication_subscriptions_publication_fkey" 712 745 columns: ["publication"]
+61
supabase/migrations/20250610232213_add_bsky_profiles_and_foreign_key_to_subscriptions.sql
··· 1 + create table "public"."bsky_profiles" ( 2 + "did" text not null, 3 + "record" jsonb not null, 4 + "indexed_at" timestamp with time zone not null default now() 5 + ); 6 + 7 + alter table "public"."bsky_profiles" enable row level security; 8 + 9 + CREATE UNIQUE INDEX bsky_profiles_pkey ON public.bsky_profiles USING btree (did); 10 + 11 + alter table "public"."bsky_profiles" add constraint "bsky_profiles_pkey" PRIMARY KEY using index "bsky_profiles_pkey"; 12 + 13 + alter table "public"."bsky_profiles" add constraint "bsky_profiles_did_fkey" FOREIGN KEY (did) REFERENCES identities(atp_did) ON DELETE CASCADE not valid; 14 + 15 + alter table "public"."bsky_profiles" validate constraint "bsky_profiles_did_fkey"; 16 + 17 + alter table "public"."publication_subscriptions" add constraint "publication_subscriptions_identity_fkey" FOREIGN KEY (identity) REFERENCES identities(atp_did) ON DELETE CASCADE not valid; 18 + 19 + alter table "public"."publication_subscriptions" validate constraint "publication_subscriptions_identity_fkey"; 20 + 21 + grant delete on table "public"."bsky_profiles" to "anon"; 22 + 23 + grant insert on table "public"."bsky_profiles" to "anon"; 24 + 25 + grant references on table "public"."bsky_profiles" to "anon"; 26 + 27 + grant select on table "public"."bsky_profiles" to "anon"; 28 + 29 + grant trigger on table "public"."bsky_profiles" to "anon"; 30 + 31 + grant truncate on table "public"."bsky_profiles" to "anon"; 32 + 33 + grant update on table "public"."bsky_profiles" to "anon"; 34 + 35 + grant delete on table "public"."bsky_profiles" to "authenticated"; 36 + 37 + grant insert on table "public"."bsky_profiles" to "authenticated"; 38 + 39 + grant references on table "public"."bsky_profiles" to "authenticated"; 40 + 41 + grant select on table "public"."bsky_profiles" to "authenticated"; 42 + 43 + grant trigger on table "public"."bsky_profiles" to "authenticated"; 44 + 45 + grant truncate on table "public"."bsky_profiles" to "authenticated"; 46 + 47 + grant update on table "public"."bsky_profiles" to "authenticated"; 48 + 49 + grant delete on table "public"."bsky_profiles" to "service_role"; 50 + 51 + grant insert on table "public"."bsky_profiles" to "service_role"; 52 + 53 + grant references on table "public"."bsky_profiles" to "service_role"; 54 + 55 + grant select on table "public"."bsky_profiles" to "service_role"; 56 + 57 + grant trigger on table "public"."bsky_profiles" to "service_role"; 58 + 59 + grant truncate on table "public"."bsky_profiles" to "service_role"; 60 + 61 + grant update on table "public"."bsky_profiles" to "service_role";