PlatformKit by Septagon
guides

Secure Extensions

PlatformKit has one supported application-extension seam:

Source
docs/current/extensions.md
Route
/docs/current-extensions

Build a secure extension

PlatformKit has one supported application-extension seam: starterapp.WithModules.

The fastest correct start is to generate one:

platformkit new module invoice

The generated module already embodies every rule below — tenant-scoped queries, per-route scope checks, server-owned identity, canonical entity IDs, append-only migrations — and ships a test that fails the moment tenant isolation breaks. It registers itself, so adding a module never edits main.go.

To read the same thing by hand, see pk-apps/reference/custommodule. That directory is a runnable teaching reference, not a shipped product, module, or alternate starter.

The reference demonstrates the required defaults:

  1. Build the module on ModuleEnv.DB, the starter's shared connection pool.

The pool is whichever engine the application configured — SQLite or Postgres — so write portable SQL, or ship an adapter per engine the way the built-in modules do.

  1. Apply append-only, embedded migrations and record each applied filename.
  2. Declare every machine capability in ModulePlugin.APIKeyScopes.
  3. Enforce the matching scope in every authenticated route, while allowing the

reserved interactive admin scope where appropriate.

  1. Obtain tenant and subject from portslib.RequestActor; never trust body or

query identity.

  1. Generate IDs and timestamps on the server.
  2. Scope every read, update, and delete query by tenant.
  3. Reject unknown JSON fields and trailing JSON values.
  4. Publish route metadata through OpenAPIOperation.
  5. Test anonymous access, insufficient scopes, server-owned identity,

cross-tenant reads, migrations, and the happy path.

Authentication is not authorization

RegisterRoutes inherits identity resolution, the anonymous-mutation gate, and the request-body limit. It does not infer a domain policy for your module. A route that only calls RequestActor is authenticated but not authorized.

Declare application scopes:

return starterapp.ModulePlugin{
    ID:             "reservation",
    RegisterRoutes: handler.RegisterRoutes,
    APIKeyScopes: []string{
        "reservations:read",
        "reservations:write",
    },
}

Then enforce one in the handler:

tenantID, subject, ok := portslib.RequestActor(w, r)
if !ok {
    return
}
principal := identity.PrincipalFromContext(r.Context())
if !principal.HasScope("admin") &&
    !principal.HasScope("reservations:write") {
    http.Error(w, "forbidden: reservations:write scope required", http.StatusForbidden)
    return
}

Unknown API-key scopes are rejected. Declaring the scope in APIKeyScopes makes it issuable; it does not authorize a route by itself.

Keep product code downstream

Put the module's domain model, routes, migrations, policy, and tests in the repository that owns the application. PlatformKit remains a generic foundation whether the downstream product is a marketplace, CRM, booking system, or internal tool.