agentleFS
willdadywilldady/platypus/.agents/skills/add-api-endpoint/SKILL.md

add-api-endpoint

Skill130 starsChanged 6 days agoMITView on GitHub

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

4 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

4 rules. 0 are common, 2 are distinctive, and 2 are very specific to this project.

  • 0

    Common

    Most of the 4 edited versions we read keep these.

  • 2

    Distinctive

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

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

    line 49
  2. Very specific:

    Read the resolved scope through the scope accessors (workspaceScopeOf, orgScopeOf) rather than re-reading path params: the accessor hands …

    line 70
  3. Very specific:

    Import the shared db instance from the backend entry module, the way the route modules already do, and query it with Drizzle.

    line 79
  4. Distinctive:

    Never scope a query by a raw path param.

    line 83

“Common” is measured: kept by at least 6 in 10 of the 4 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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,
      validate("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(null)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. Likewise read the session user and Org membership through userOf and orgMembershipOf, never c.get("user")!: they throw (a 500) when the middleware is missing from the route.

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.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.