Exosphere is a set of small, modular, self-hostable community tools built on the AT Protocol.
app.exosphere.site
Multi-Sphere Code Review#
Warnings#
api.ts / sphere.ts — circular import#
- File:
packages/client/src/api.ts:2andpackages/client/src/sphere.ts:3 - Issue:
api.tsimportssphereSlugfromsphere.ts, whilesphere.tsimportsapiFetchfromapi.ts. This works with ESM live bindings since neither import is used at module-initialization time, but it's fragile — any future top-level usage in either file will break. - Fix: Move
moduleFetchto its own file (e.g.module-api.ts) that imports from bothapi.tsandsphere.ts, breaking the cycle.
SphereLoader reads signal during render, causing unnecessary re-renders#
- File:
packages/app/src/app.tsx:73 - Issue:
const currentSlug = sphereSlug.valuesubscribes the component tosphereStatechanges. SinceSphereLoaderreturnsnull, these re-renders are wasted work. - Fix: Read the signal inside the effect:
function SphereLoader() {
const { params } = useRoute();
useEffect(() => {
const urlSlug = params.sphereSlug;
if (urlSlug && urlSlug !== sphereSlug.peek()) {
loadSphere(urlSlug);
}
}, [params.sphereSlug]);
return null;
}
sphereId has no FK constraint#
- File:
packages/feature-requests/src/db/schema.ts:15 - Issue:
sphereId: text("sphere_id").notNull()doesn't reference thespherestable. If a sphere is deleted, orphaned feature requests would remain with no referential integrity enforcement. - Fix: Add a foreign key reference (both tables are in the same SQLite database):
import { spheres } from "@exosphere/core/db/schema";
// ...
sphereId: text("sphere_id").notNull().references(() => spheres.id),
DELETE /:id/vote not sphere-scoped#
- File:
packages/feature-requests/src/api/votes.ts - Issue: The unvote handler (and similarly
DELETE /comments/:id/vote) doesn't verify the vote's feature request belongs to the current sphere. A user could callDELETE /api/s/sphere-a/feature-requests/{id}/voteto remove a vote on a feature request that belongs tosphere-b. Not exploitable (the result is the same — the vote is removed), but inconsistent with the other endpoints that all verify sphere membership. - Fix: Add
eq(featureRequests.sphereId, sphereId)to the vote lookup query in the DELETE handler, same as the POST handler.
Suggestions#
Mutable module-level state shared across SSR requests#
- File:
packages/client/src/config.ts - Issue:
export let isMultiSphereis module-level mutable state. In SSR,setMultiSphereis called before each render, and since Bun is single-threaded with synchronous prerender, this is safe today. But if SSR ever becomes concurrent, this becomes a race. Worth a// NOTE:comment documenting this assumption.
Empty render when pending && !loading#
- File:
packages/app/src/pages/dashboard.tsx:32 - Issue: When
pendingis true butloadingis false (the brief initial state before the loading delay timer fires), the sphere list area rendersnull— the page shows the title and button but no content area at all. For SSR, this means the server-rendered HTML contains only the heading. - Fix: Consider rendering a minimal placeholder or skipping the loading delay for SSR.
Summary#
Overall the changes are clean and well-structured. The sphere context middleware centralizes what was previously scattered sphere lookups across handlers. The moduleFetch abstraction is a nice simplification for the client API layer. The migration approach (regenerating the initial migration) is fine for a pre-launch project.