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

Configure Feed

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

Multi-Sphere Code Review#

Warnings#

api.ts / sphere.ts — circular import#

  • File: packages/client/src/api.ts:2 and packages/client/src/sphere.ts:3
  • Issue: api.ts imports sphereSlug from sphere.ts, while sphere.ts imports apiFetch from api.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 moduleFetch to its own file (e.g. module-api.ts) that imports from both api.ts and sphere.ts, breaking the cycle.

SphereLoader reads signal during render, causing unnecessary re-renders#

  • File: packages/app/src/app.tsx:73
  • Issue: const currentSlug = sphereSlug.value subscribes the component to sphereState changes. Since SphereLoader returns null, 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 the spheres table. 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 call DELETE /api/s/sphere-a/feature-requests/{id}/vote to remove a vote on a feature request that belongs to sphere-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 isMultiSphere is module-level mutable state. In SSR, setMultiSphere is 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 pending is true but loading is false (the brief initial state before the loading delay timer fires), the sphere list area renders null — 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.