Exosphere is a set of small, modular, self-hostable community tools built on the AT Protocol. app.exosphere.site
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: remove new spheres and indexing

Hugo 51cf37f5 a0ba82ca

+205 -163
+33 -103
drizzle/meta/0000_snapshot.json
··· 175 175 "indexes": { 176 176 "idx_sphere_members_did": { 177 177 "name": "idx_sphere_members_did", 178 - "columns": [ 179 - "did" 180 - ], 178 + "columns": ["did"], 181 179 "isUnique": false 182 180 } 183 181 }, ··· 186 184 "name": "sphere_members_sphere_id_spheres_id_fk", 187 185 "tableFrom": "sphere_members", 188 186 "tableTo": "spheres", 189 - "columnsFrom": [ 190 - "sphere_id" 191 - ], 192 - "columnsTo": [ 193 - "id" 194 - ], 187 + "columnsFrom": ["sphere_id"], 188 + "columnsTo": ["id"], 195 189 "onDelete": "no action", 196 190 "onUpdate": "no action" 197 191 } 198 192 }, 199 193 "compositePrimaryKeys": { 200 194 "sphere_members_sphere_id_did_pk": { 201 - "columns": [ 202 - "sphere_id", 203 - "did" 204 - ], 195 + "columns": ["sphere_id", "did"], 205 196 "name": "sphere_members_sphere_id_did_pk" 206 197 } 207 198 }, ··· 240 231 "name": "sphere_modules_sphere_id_spheres_id_fk", 241 232 "tableFrom": "sphere_modules", 242 233 "tableTo": "spheres", 243 - "columnsFrom": [ 244 - "sphere_id" 245 - ], 246 - "columnsTo": [ 247 - "id" 248 - ], 234 + "columnsFrom": ["sphere_id"], 235 + "columnsTo": ["id"], 249 236 "onDelete": "no action", 250 237 "onUpdate": "no action" 251 238 } 252 239 }, 253 240 "compositePrimaryKeys": { 254 241 "sphere_modules_sphere_id_module_name_pk": { 255 - "columns": [ 256 - "sphere_id", 257 - "module_name" 258 - ], 242 + "columns": ["sphere_id", "module_name"], 259 243 "name": "sphere_modules_sphere_id_module_name_pk" 260 244 } 261 245 }, ··· 343 327 "indexes": { 344 328 "spheres_handle_unique": { 345 329 "name": "spheres_handle_unique", 346 - "columns": [ 347 - "handle" 348 - ], 330 + "columns": ["handle"], 349 331 "isUnique": true 350 332 } 351 333 }, ··· 390 372 "indexes": { 391 373 "idx_feature_request_comment_votes_comment": { 392 374 "name": "idx_feature_request_comment_votes_comment", 393 - "columns": [ 394 - "comment_id" 395 - ], 375 + "columns": ["comment_id"], 396 376 "isUnique": false 397 377 } 398 378 }, ··· 401 381 "name": "feature_request_comment_votes_comment_id_feature_request_comments_id_fk", 402 382 "tableFrom": "feature_request_comment_votes", 403 383 "tableTo": "feature_request_comments", 404 - "columnsFrom": [ 405 - "comment_id" 406 - ], 407 - "columnsTo": [ 408 - "id" 409 - ], 384 + "columnsFrom": ["comment_id"], 385 + "columnsTo": ["id"], 410 386 "onDelete": "no action", 411 387 "onUpdate": "no action" 412 388 } 413 389 }, 414 390 "compositePrimaryKeys": { 415 391 "feature_request_comment_votes_comment_id_author_did_pk": { 416 - "columns": [ 417 - "comment_id", 418 - "author_did" 419 - ], 392 + "columns": ["comment_id", "author_did"], 420 393 "name": "feature_request_comment_votes_comment_id_author_did_pk" 421 394 } 422 395 }, ··· 495 468 "indexes": { 496 469 "idx_feature_request_comments_request": { 497 470 "name": "idx_feature_request_comments_request", 498 - "columns": [ 499 - "request_id" 500 - ], 471 + "columns": ["request_id"], 501 472 "isUnique": false 502 473 }, 503 474 "idx_feature_request_comments_author_request": { 504 475 "name": "idx_feature_request_comments_author_request", 505 - "columns": [ 506 - "author_did", 507 - "request_id" 508 - ], 476 + "columns": ["author_did", "request_id"], 509 477 "isUnique": false 510 478 } 511 479 }, ··· 514 482 "name": "feature_request_comments_request_id_feature_requests_id_fk", 515 483 "tableFrom": "feature_request_comments", 516 484 "tableTo": "feature_requests", 517 - "columnsFrom": [ 518 - "request_id" 519 - ], 520 - "columnsTo": [ 521 - "id" 522 - ], 485 + "columnsFrom": ["request_id"], 486 + "columnsTo": ["id"], 523 487 "onDelete": "no action", 524 488 "onUpdate": "no action" 525 489 } ··· 578 542 "indexes": { 579 543 "idx_feature_request_statuses_request": { 580 544 "name": "idx_feature_request_statuses_request", 581 - "columns": [ 582 - "request_id" 583 - ], 545 + "columns": ["request_id"], 584 546 "isUnique": false 585 547 } 586 548 }, ··· 589 551 "name": "feature_request_statuses_request_id_feature_requests_id_fk", 590 552 "tableFrom": "feature_request_statuses", 591 553 "tableTo": "feature_requests", 592 - "columnsFrom": [ 593 - "request_id" 594 - ], 595 - "columnsTo": [ 596 - "id" 597 - ], 554 + "columnsFrom": ["request_id"], 555 + "columnsTo": ["id"], 598 556 "onDelete": "no action", 599 557 "onUpdate": "no action" 600 558 } ··· 639 597 "indexes": { 640 598 "idx_feature_request_votes_request": { 641 599 "name": "idx_feature_request_votes_request", 642 - "columns": [ 643 - "request_id" 644 - ], 600 + "columns": ["request_id"], 645 601 "isUnique": false 646 602 } 647 603 }, ··· 650 606 "name": "feature_request_votes_request_id_feature_requests_id_fk", 651 607 "tableFrom": "feature_request_votes", 652 608 "tableTo": "feature_requests", 653 - "columnsFrom": [ 654 - "request_id" 655 - ], 656 - "columnsTo": [ 657 - "id" 658 - ], 609 + "columnsFrom": ["request_id"], 610 + "columnsTo": ["id"], 659 611 "onDelete": "no action", 660 612 "onUpdate": "no action" 661 613 } 662 614 }, 663 615 "compositePrimaryKeys": { 664 616 "feature_request_votes_request_id_author_did_pk": { 665 - "columns": [ 666 - "request_id", 667 - "author_did" 668 - ], 617 + "columns": ["request_id", "author_did"], 669 618 "name": "feature_request_votes_request_id_author_did_pk" 670 619 } 671 620 }, ··· 781 730 "indexes": { 782 731 "idx_feature_requests_sphere_number": { 783 732 "name": "idx_feature_requests_sphere_number", 784 - "columns": [ 785 - "sphere_id", 786 - "number" 787 - ], 733 + "columns": ["sphere_id", "number"], 788 734 "isUnique": true 789 735 }, 790 736 "idx_feature_requests_sphere": { 791 737 "name": "idx_feature_requests_sphere", 792 - "columns": [ 793 - "sphere_id" 794 - ], 738 + "columns": ["sphere_id"], 795 739 "isUnique": false 796 740 }, 797 741 "idx_feature_requests_status": { 798 742 "name": "idx_feature_requests_status", 799 - "columns": [ 800 - "status" 801 - ], 743 + "columns": ["status"], 802 744 "isUnique": false 803 745 }, 804 746 "idx_feature_requests_created": { 805 747 "name": "idx_feature_requests_created", 806 - "columns": [ 807 - "created_at" 808 - ], 748 + "columns": ["created_at"], 809 749 "isUnique": false 810 750 }, 811 751 "idx_feature_requests_category": { 812 752 "name": "idx_feature_requests_category", 813 - "columns": [ 814 - "category" 815 - ], 753 + "columns": ["category"], 816 754 "isUnique": false 817 755 } 818 756 }, ··· 821 759 "name": "feature_requests_sphere_id_spheres_id_fk", 822 760 "tableFrom": "feature_requests", 823 761 "tableTo": "spheres", 824 - "columnsFrom": [ 825 - "sphere_id" 826 - ], 827 - "columnsTo": [ 828 - "id" 829 - ], 762 + "columnsFrom": ["sphere_id"], 763 + "columnsTo": ["id"], 830 764 "onDelete": "no action", 831 765 "onUpdate": "no action" 832 766 } ··· 893 827 "indexes": { 894 828 "idx_feed_posts_parent": { 895 829 "name": "idx_feed_posts_parent", 896 - "columns": [ 897 - "parent_id" 898 - ], 830 + "columns": ["parent_id"], 899 831 "isUnique": false 900 832 }, 901 833 "idx_feed_posts_created": { 902 834 "name": "idx_feed_posts_created", 903 - "columns": [ 904 - "created_at" 905 - ], 835 + "columns": ["created_at"], 906 836 "isUnique": false 907 837 } 908 838 }, ··· 922 852 "internal": { 923 853 "indexes": {} 924 854 } 925 - } 855 + }
+1 -1
drizzle/meta/_journal.json
··· 10 10 "breakpoints": true 11 11 } 12 12 ] 13 - } 13 + }
+5 -1
packages/app/e2e/global-setup.ts
··· 8 8 // - SSR calls setMultiSphere(data.multiSphere) before rendering 9 9 // - Client calls setMultiSphere(ssrData.multiSphere) before hydration 10 10 // The build-time __MULTI_SPHERE__ is only a fallback for dev without SSR. 11 - execSync("bun run build", { stdio: "inherit", cwd: ROOT, env: { ...process.env, MULTI_SPHERE: "" } }); 11 + execSync("bun run build", { 12 + stdio: "inherit", 13 + cwd: ROOT, 14 + env: { ...process.env, MULTI_SPHERE: "" }, 15 + }); 12 16 13 17 // Seed the test database 14 18 const seedScript = resolve(import.meta.dirname, "seed.ts");
+149 -27
packages/app/e2e/seed.ts
··· 68 68 ); 69 69 70 70 const featureRequests = [ 71 - { id: "fr-001", number: 1, title: "Add dark mode support", description: "It would be great to have a dark mode option for better readability at night.", category: "enhancement", status: "approved", authorDid: MEMBER_DID }, 72 - { id: "fr-002", number: 2, title: "Export data as CSV", description: "Allow users to export their data in CSV format for analysis.", category: "general", status: "requested", authorDid: OWNER_DID }, 73 - { id: "fr-003", number: 3, title: "Mobile app", description: "A native mobile application would improve the user experience significantly.", category: "general", status: "requested", authorDid: MEMBER_DID }, 74 - { id: "fr-004", number: 4, title: "Keyboard shortcuts", description: "Add keyboard shortcuts for common actions like voting and navigation.", category: "enhancement", status: "done", authorDid: OWNER_DID }, 75 - { id: "fr-005", number: 5, title: "Windows phone support", description: "Please add support for Windows Phone platform.", category: "general", status: "not-planned", authorDid: MEMBER_DID }, 71 + { 72 + id: "fr-001", 73 + number: 1, 74 + title: "Add dark mode support", 75 + description: "It would be great to have a dark mode option for better readability at night.", 76 + category: "enhancement", 77 + status: "approved", 78 + authorDid: MEMBER_DID, 79 + }, 80 + { 81 + id: "fr-002", 82 + number: 2, 83 + title: "Export data as CSV", 84 + description: "Allow users to export their data in CSV format for analysis.", 85 + category: "general", 86 + status: "requested", 87 + authorDid: OWNER_DID, 88 + }, 89 + { 90 + id: "fr-003", 91 + number: 3, 92 + title: "Mobile app", 93 + description: "A native mobile application would improve the user experience significantly.", 94 + category: "general", 95 + status: "requested", 96 + authorDid: MEMBER_DID, 97 + }, 98 + { 99 + id: "fr-004", 100 + number: 4, 101 + title: "Keyboard shortcuts", 102 + description: "Add keyboard shortcuts for common actions like voting and navigation.", 103 + category: "enhancement", 104 + status: "done", 105 + authorDid: OWNER_DID, 106 + }, 107 + { 108 + id: "fr-005", 109 + number: 5, 110 + title: "Windows phone support", 111 + description: "Please add support for Windows Phone platform.", 112 + category: "general", 113 + status: "not-planned", 114 + authorDid: MEMBER_DID, 115 + }, 76 116 ]; 77 117 78 118 for (const fr of featureRequests) { ··· 83 123 ); 84 124 } 85 125 86 - db.run(`INSERT INTO feature_request_votes (request_id, author_did) VALUES (?, ?)`, ["fr-001", OWNER_DID]); 87 - db.run(`INSERT INTO feature_request_votes (request_id, author_did) VALUES (?, ?)`, ["fr-001", MEMBER_DID]); 88 - db.run(`INSERT INTO feature_request_votes (request_id, author_did) VALUES (?, ?)`, ["fr-002", MEMBER_DID]); 126 + db.run(`INSERT INTO feature_request_votes (request_id, author_did) VALUES (?, ?)`, [ 127 + "fr-001", 128 + OWNER_DID, 129 + ]); 130 + db.run(`INSERT INTO feature_request_votes (request_id, author_did) VALUES (?, ?)`, [ 131 + "fr-001", 132 + MEMBER_DID, 133 + ]); 134 + db.run(`INSERT INTO feature_request_votes (request_id, author_did) VALUES (?, ?)`, [ 135 + "fr-002", 136 + MEMBER_DID, 137 + ]); 89 138 90 139 db.run( 91 140 `INSERT INTO feature_request_comments (id, request_id, author_did, content) VALUES (?, ?, ?, ?)`, ··· 117 166 `INSERT INTO sphere_members (sphere_id, did, role, status) VALUES (?, ?, 'owner', 'active')`, 118 167 ["sphere-alpha", OWNER_DID], 119 168 ); 120 - db.run( 121 - `INSERT INTO sphere_modules (sphere_id, module_name) VALUES (?, 'feature-requests')`, 122 - ["sphere-alpha"], 123 - ); 169 + db.run(`INSERT INTO sphere_modules (sphere_id, module_name) VALUES (?, 'feature-requests')`, [ 170 + "sphere-alpha", 171 + ]); 124 172 125 173 // Alpha feature requests (#1-#4 — per-sphere numbering, covers all status tabs) 126 174 const alphaRequests = [ 127 - { id: "alpha-fr-001", number: 1, title: "Alpha dark mode", description: "Dark mode for Alpha sphere.", category: "enhancement", status: "approved", authorDid: OWNER_DID }, 128 - { id: "alpha-fr-002", number: 2, title: "Alpha CSV export", description: "CSV export for Alpha sphere.", category: "general", status: "requested", authorDid: MEMBER_DID }, 129 - { id: "alpha-fr-003", number: 3, title: "Alpha done feature", description: "A completed feature.", category: "general", status: "done", authorDid: OWNER_DID }, 130 - { id: "alpha-fr-004", number: 4, title: "Alpha rejected idea", description: "A rejected feature.", category: "general", status: "not-planned", authorDid: MEMBER_DID }, 175 + { 176 + id: "alpha-fr-001", 177 + number: 1, 178 + title: "Alpha dark mode", 179 + description: "Dark mode for Alpha sphere.", 180 + category: "enhancement", 181 + status: "approved", 182 + authorDid: OWNER_DID, 183 + }, 184 + { 185 + id: "alpha-fr-002", 186 + number: 2, 187 + title: "Alpha CSV export", 188 + description: "CSV export for Alpha sphere.", 189 + category: "general", 190 + status: "requested", 191 + authorDid: MEMBER_DID, 192 + }, 193 + { 194 + id: "alpha-fr-003", 195 + number: 3, 196 + title: "Alpha done feature", 197 + description: "A completed feature.", 198 + category: "general", 199 + status: "done", 200 + authorDid: OWNER_DID, 201 + }, 202 + { 203 + id: "alpha-fr-004", 204 + number: 4, 205 + title: "Alpha rejected idea", 206 + description: "A rejected feature.", 207 + category: "general", 208 + status: "not-planned", 209 + authorDid: MEMBER_DID, 210 + }, 131 211 ]; 132 212 133 213 for (const fr of alphaRequests) { 134 214 db.run( 135 215 `INSERT INTO feature_requests (id, sphere_id, number, title, description, category, status, author_did) 136 216 VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, 137 - [fr.id, "sphere-alpha", fr.number, fr.title, fr.description, fr.category, fr.status, fr.authorDid], 217 + [ 218 + fr.id, 219 + "sphere-alpha", 220 + fr.number, 221 + fr.title, 222 + fr.description, 223 + fr.category, 224 + fr.status, 225 + fr.authorDid, 226 + ], 138 227 ); 139 228 } 140 229 141 - db.run(`INSERT INTO feature_request_votes (request_id, author_did) VALUES (?, ?)`, ["alpha-fr-001", OWNER_DID]); 142 - db.run(`INSERT INTO feature_request_votes (request_id, author_did) VALUES (?, ?)`, ["alpha-fr-001", MEMBER_DID]); 230 + db.run(`INSERT INTO feature_request_votes (request_id, author_did) VALUES (?, ?)`, [ 231 + "alpha-fr-001", 232 + OWNER_DID, 233 + ]); 234 + db.run(`INSERT INTO feature_request_votes (request_id, author_did) VALUES (?, ?)`, [ 235 + "alpha-fr-001", 236 + MEMBER_DID, 237 + ]); 143 238 144 239 db.run( 145 240 `INSERT INTO feature_request_comments (id, request_id, author_did, content) VALUES (?, ?, ?, ?)`, ··· 157 252 `INSERT INTO sphere_members (sphere_id, did, role, status) VALUES (?, ?, 'owner', 'active')`, 158 253 ["sphere-beta", MEMBER_DID], 159 254 ); 160 - db.run( 161 - `INSERT INTO sphere_modules (sphere_id, module_name) VALUES (?, 'feature-requests')`, 162 - ["sphere-beta"], 163 - ); 255 + db.run(`INSERT INTO sphere_modules (sphere_id, module_name) VALUES (?, 'feature-requests')`, [ 256 + "sphere-beta", 257 + ]); 164 258 165 259 // Beta feature requests (#1-#2 — independent numbering from Alpha) 166 260 const betaRequests = [ 167 - { id: "beta-fr-001", number: 1, title: "Beta mobile app", description: "Mobile app for Beta sphere.", category: "general", status: "requested", authorDid: MEMBER_DID }, 168 - { id: "beta-fr-002", number: 2, title: "Beta API access", description: "Public API for Beta sphere.", category: "enhancement", status: "approved", authorDid: OWNER_DID }, 261 + { 262 + id: "beta-fr-001", 263 + number: 1, 264 + title: "Beta mobile app", 265 + description: "Mobile app for Beta sphere.", 266 + category: "general", 267 + status: "requested", 268 + authorDid: MEMBER_DID, 269 + }, 270 + { 271 + id: "beta-fr-002", 272 + number: 2, 273 + title: "Beta API access", 274 + description: "Public API for Beta sphere.", 275 + category: "enhancement", 276 + status: "approved", 277 + authorDid: OWNER_DID, 278 + }, 169 279 ]; 170 280 171 281 for (const fr of betaRequests) { 172 282 db.run( 173 283 `INSERT INTO feature_requests (id, sphere_id, number, title, description, category, status, author_did) 174 284 VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, 175 - [fr.id, "sphere-beta", fr.number, fr.title, fr.description, fr.category, fr.status, fr.authorDid], 285 + [ 286 + fr.id, 287 + "sphere-beta", 288 + fr.number, 289 + fr.title, 290 + fr.description, 291 + fr.category, 292 + fr.status, 293 + fr.authorDid, 294 + ], 176 295 ); 177 296 } 178 297 179 - db.run(`INSERT INTO feature_request_votes (request_id, author_did) VALUES (?, ?)`, ["beta-fr-001", MEMBER_DID]); 298 + db.run(`INSERT INTO feature_request_votes (request_id, author_did) VALUES (?, ?)`, [ 299 + "beta-fr-001", 300 + MEMBER_DID, 301 + ]); 180 302 181 303 db.close(); 182 304 console.log("[e2e-seed] Multi-sphere database seeded");
+3 -1
packages/app/src/app.tsx
··· 109 109 <Router> 110 110 <Route path="/sign-in" component={SignInPage} /> 111 111 <Route path="/spheres/new" component={CreateSphereGuarded} /> 112 - {routes.map((r) => <Route key={r.path} path={r.path} component={r.component} />)} 112 + {routes.map((r) => ( 113 + <Route key={r.path} path={r.path} component={r.component} /> 114 + ))} 113 115 <Route path="/s/:sphereHandle" component={withSphereLoader(SpherePage)} /> 114 116 <Route path="/" component={MultiSphereDefaultPage} /> 115 117 <Route default component={MultiSphereDefaultPage} />
+6 -1
packages/core/src/__tests__/sphere-operations.test.ts
··· 84 84 upsertSphereFromRecord({ 85 85 did: OWNER_DID, 86 86 rkey: SPHERE_ID, 87 - record: { handle: SPHERE_HANDLE, name: "My Sphere", visibility: "public", writeAccess: "open" }, 87 + record: { 88 + handle: SPHERE_HANDLE, 89 + name: "My Sphere", 90 + visibility: "public", 91 + writeAccess: "open", 92 + }, 88 93 pdsUri: "at://did:plc:owner1/com.exosphere.sphere/sphere-1", 89 94 }); 90 95
+3 -19
packages/core/src/sphere/operations.ts
··· 80 80 81 81 db.update(spheres).set(set).where(eq(spheres.id, existing.id)).run(); 82 82 } else { 83 - db.insert(spheres) 84 - .values({ 85 - id: rkey, 86 - handle, 87 - name: (record.name as string) ?? handle, 88 - description: (record.description as string) ?? null, 89 - visibility: (record.visibility as "public" | "private") ?? "public", 90 - writeAccess: (record.writeAccess as "open" | "members") ?? "open", 91 - ownerDid: did, 92 - pdsUri, 93 - }) 94 - .onConflictDoNothing() 95 - .run(); 96 - 97 - // Auto-add owner as active member 98 - db.insert(sphereMembers) 99 - .values({ sphereId: rkey, did, role: "owner", status: "active" }) 100 - .onConflictDoNothing() 101 - .run(); 83 + // Ignore sphere records for handles not on this instance — 84 + // spheres are created locally, not via Jetstream from other instances. 85 + return; 102 86 } 103 87 } 104 88
+1 -3
packages/feature-requests/src/__tests__/schemas.test.ts
··· 85 85 }); 86 86 87 87 it("rejects content over 5 000 chars", () => { 88 - expect(() => 89 - createCommentSchema.parse({ content: "x".repeat(5001) }), 90 - ).toThrow(); 88 + expect(() => createCommentSchema.parse({ content: "x".repeat(5001) })).toThrow(); 91 89 }); 92 90 }); 93 91
+1 -3
packages/feature-requests/src/api/comments.ts
··· 36 36 .select() 37 37 .from(featureRequestComments) 38 38 .innerJoin(featureRequests, eq(featureRequests.id, featureRequestComments.requestId)) 39 - .where( 40 - and(eq(featureRequestComments.id, commentId), eq(featureRequests.sphereId, sphereId)), 41 - ) 39 + .where(and(eq(featureRequestComments.id, commentId), eq(featureRequests.sphereId, sphereId))) 42 40 .get(); 43 41 } 44 42
+1 -4
packages/feature-requests/src/api/votes.ts
··· 19 19 .from(featureRequestVotes) 20 20 .innerJoin(featureRequests, eq(featureRequests.id, featureRequestVotes.requestId)) 21 21 .where( 22 - and( 23 - eq(featureRequestVotes.authorDid, c.var.did), 24 - eq(featureRequests.sphereId, sphereId), 25 - ), 22 + and(eq(featureRequestVotes.authorDid, c.var.did), eq(featureRequests.sphereId, sphereId)), 26 23 ) 27 24 .all(); 28 25 return c.json({ votes: rows.map((r) => r.requestId) });
+2
review.md
··· 13 13 - **File**: `packages/app/src/app.tsx:73` 14 14 - **Issue**: `const currentSlug = sphereSlug.value` subscribes the component to `sphereState` changes. Since `SphereLoader` returns `null`, these re-renders are wasted work. 15 15 - **Fix**: Read the signal inside the effect: 16 + 16 17 ```tsx 17 18 function SphereLoader() { 18 19 const { params } = useRoute(); ··· 31 32 - **File**: `packages/feature-requests/src/db/schema.ts:15` 32 33 - **Issue**: `sphereId: text("sphere_id").notNull()` doesn't reference the `spheres` table. If a sphere is deleted, orphaned feature requests would remain with no referential integrity enforcement. 33 34 - **Fix**: Add a foreign key reference (both tables are in the same SQLite database): 35 + 34 36 ```ts 35 37 import { spheres } from "@exosphere/core/db/schema"; 36 38 // ...