LLMSkillHub
mirroredCopied from willdady/platypus under MIT. willdady has not claimed this page, and is not affiliated with LLM SkillHub.

willdady / add-api-endpoint

Guide for adding new API endpoints to the Platypus backend using Hono.js — routing, tenant scoping middleware, validation, and database access.

0
SKILL.md
3.7 KB

Adding New API Endpoints

Authenticated is not scoped. requireAuth proves who the caller is and
nothing about what they may reach. Platypus is multi-tenant — Organization →
Workspace → resource — so an endpoint carrying only requireAuth is reachable
by any logged-in user against any tenant's data. Scope is a separate,
mandatory decision, taken per route.

Steps

  1. Create the route module in the backend's routes area — a Hono instance
    exported for mounting.

  2. Mount it on the app in the backend's server module. The mount path
    carries the tenancy, so the hierarchy lives in the URL and the middleware
    reads the ids from it:

     app.route("/organizations/:orgId/workspaces/:workspaceId/agents", agent);
  3. Define the request schemas in the shared schemas package, following the
    full / create / update variants each domain model already has.

  4. Compose each route: authenticate, scope, validate, handle — in that
    order, as arguments to the route, not as a blanket .use("*"). Scope is per
    route because two routes in one file rarely deserve the same access.

     agent.post(
       "/",
       requireAuth,
       requireOrgAccess(),
       requireWorkspaceAccess,
       sValidator("json", agentCreateSchema),
       async (c) => {
         const data = c.req.valid("json");
         const scope = workspaceScopeOf(c);
         // ...
       },
     );

Done when every route in the module names its scoping middleware
explicitly, and you can say which tenant each one is confined to.

Choosing the scope

Pick the narrowest that admits the callers you intend:

MiddlewareAdmits
requireOrgAccess()any member of the Organization in the path
requireOrgAccess(["admin"])members holding one of the named Organization roles
requireWorkspaceAccesscallers permitted in the Workspace in the path
requireWorkspaceOwnerthe Workspace's Owner
requireWorkspaceConfigAccess()credential- or reach-bearing Workspace config, admin-only
requireWorkspaceConfigAccess(flag)the same, plus the Owner when that Workspace flag is set
requireSuperAdminplatform operators only

Org and Workspace scoping compose — the Workspace check assumes the Org check
ran ahead of it. Super admins bypass the Org membership check by design, so a
route may legitimately resolve with no Organization to scope to.

Read the resolved scope through the scope accessors (workspaceScopeOf,
orgScopeOf) rather than re-reading path params: the accessor hands you the
scope the middleware already decided, so the query cannot disagree with the
authorization.

Database access

Import the shared db instance from the backend entry module, the way the
route modules already do, and query it with Drizzle. The request context also
carries a db, but that is a narrow exception rather than the house pattern.

Never scope a query by a raw path param. Take the tenant ids from the resolved
scope, so an unscoped read cannot be written by accident.

Responses

Response body shape — the error key, the message key, and when to throw a
typed error instead of returning one — is covered by the api-conventions
skill. Load it rather than inventing a shape.