Gram's management API is the internal HTTP-RPC surface that the dashboard, CLI, and SDK use to administer projects, toolsets, deployments, access, and related resources. Every endpoint lives at /rpc/<service>.<method>, is authored in Goa DSL under server/design/, implemented in a single Service struct per package under server/internal/<service>/, and exposed through generated server stubs, OpenAPI, CLI bindings, and a TypeScript SDK.
Concepts and terminology
Service. A named collection of related endpoints (e.g. remoteMcp, access, auditlogs). Each service maps one-to-one to a Go package of the same name.
Method. A single endpoint on a service. Exposed as /rpc/<service>.<method>.
Payload / Result. The input and output types for a method. Payloads are composed from shared security payloads plus method-specific form attributes.
Security scheme. The authentication mechanism a method accepts. Gram's management endpoints use three schemes: Session (browser cookie), ByKey (API key header), and ProjectSlug (project-selector header). Additional schemes exist for non-management surfaces and are out of scope here.
Model views (mv). Stateless functions that convert database row types into API response types. Keep database types out of the API boundary — handlers always return a view, never a repo struct.
Handler. One method implementation on the Service struct.
Management API vs public SDK. The same Goa design produces two OpenAPI outputs: an internal spec used to generate the TypeScript SDK that powers the dashboard and CLI, and a public spec derived from it via redaction overlays. Only the internal SDK sees every endpoint.
Changeset. A short changelog file written alongside a change that identifies which package bumps (server, dashboard, sdk) and by how much (patch, minor, major).
Server
The design, implementation, tests, and per-service generated code for every management endpoint live under server/.
Conventions
Naming. Goa service names (e.g. remoteMcp, auditlogs) and method names (e.g. createServer) are camelCase. DSL types (e.g. CreateServerForm, RemoteMcpServer) are PascalCase. Go package names under server/internal/ and server/design/ are lowercase with no separators (e.g. remotemcp).
Design layout. Service DSL lives at server/design/<svc>/design.go. The service package must be blank-imported in server/design/gram.go (alphabetised) for the generator to pick it up.
HTTP methods. Read methods use GET; mutations use POST. Deletes that carry only an id query parameter use DELETE.
Security DSL helpers. server/design/security/ exports the Session, ByKey, and ProjectSlug schemes plus paired helpers — SessionPayload()/SessionHeader(), ByKeyPayload()/ByKeyHeader(), ProjectPayload()/ProjectHeader() — that attach the right payload field and HTTP header to a method.
Security composition. Most management endpoints advertise Session and ByKey side-by-side via repeated Security(...) calls on the service so the dashboard and API-key clients can both reach them. Project-scoped endpoints additionally layer ProjectSlug via ProjectPayload() / ProjectHeader() so the caller must name a project.
Shared errors. Every service calls shared.DeclareErrorResponses() (from server/design/shared/errors.go) exactly once so the standard Gram error envelope applies to every method.
Shared types. When a payload or result type is reused across services, add Meta("struct:pkg:path", "types") so the generator emits it under server/gen/types/ instead of the per-service package.
OpenAPI meta keys. Every method has three metadata keys that control the downstream SDK/CLI names:
Meta("openapi:operationId", "verbNounSubject")— OpenAPI operation id.Meta("openapi:extension:x-speakeasy-name-override", "methodName")— method name on the SDK service class.Meta("openapi:extension:x-speakeasy-react-hook", '{"name": "HookName"}')— React Query hook name.
impl.go layout. Lives at server/internal/<svc>/impl.go. Contains the Service struct (injected dependencies — tracer, logger, database pool, *auth.Auth, *access.Manager, plus feature-specific fields), compile-time assertions (var _ gen.Service = (*Service)(nil), and var _ gen.Auther = (*Service)(nil) when ByKey is advertised), a NewService(...) constructor, an Attach(mux, service) wiring function, APIKeyAuth when ByKey is advertised, and one method per Goa Method declaration.
Model view files. server/internal/mv/<svc>.go (singular or per-resource — e.g. remotemcpserver.go). Exported functions are named Build<Subject>View and Build<Subject>ListView. The package doc calls these "model views", which is where the package name comes from.
Wiring. New services are attached in server/cmd/gram/start.go with <svc>.Attach(mux, <svc>.NewService(...)) near the other service attachments.
SQLc. Per-service queries live in server/internal/<svc>/queries.sql. Every new service requires a stanza in server/database/sqlc.yaml pointing at its queries file and writing to server/internal/<svc>/repo/.
Resource URNs. Every resource owned by the service gets a URN type under server/internal/urn/<resource>.go (template: server/internal/urn/api_key.go). URN types wrap uuid.UUID and give callers compile-time protection against mixing ids from different subjects. Audit event structs require them for subject identifier fields (see gram-audit-logging); other consumers can adopt them opportunistically.
Test layout. Tests live in package <svc>_test (black-box). A single setup_test.go per service defines TestMain (calling testenv.Launch) and a newTestService(t) factory that clones a fresh test database, seeds an auth context, and returns the live *Service. Package-local helpers typically include withExactAccessGrants for scope setup and requireOopsCode for error-shape assertions. One <method>_test.go per handler.
Common code flows
Handler skeleton. Inside each method: extract authCtx from context → s.access.Require(...) → validate inputs → repo work → return mv.Build<Subject>View(...). Handlers never return repo types directly. For mutation handlers, wrap the repo work in a transaction — s.db.Begin(ctx) → defer o11y.NoLogDefer(func() error { return dbtx.Rollback(ctx) }) → repo writes → audit.Log* → dbtx.Commit(ctx) — so the audit row and the state it describes commit atomically. Read handlers typically don't need a transaction or audit call.
Cascading soft-deletes. When a delete handler tombstones a parent row whose children reference it, the children have to be soft-deleted in the same handler. ON DELETE CASCADE foreign keys only fire on hard deletes, so a soft-deleted parent otherwise leaves orphan children that still resolve in the active set and point at a tombstone. Run the child cleanup inside the same dbtx as the parent write so the cascade is atomic with it. Scope the cleanup query by both the parent id and project_id (per the postgresql skill) and filter on deleted IS FALSE so re-deletes are no-ops. If the child subject is independently audited, make the cleanup query :many with RETURNING * and emit one audit.Log<Verb> per affected row before the parent's audit event — see "How to audit a cascading delete of child resources" in gram-audit-logging. Tests should both assert no active children remain pointing at the deleted parent and assert the expected child audit-event delta.
Attach wiring. func Attach(mux goahttp.Muxer, service *Service) constructs gen.NewEndpoints(service), layers the middleware.MapErrors() and middleware.TraceMethods(tracer) middleware, and mounts the endpoints via srv.Mount(mux, srv.New(endpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil)).
Non-generated files
Substitute the service name for <svc> and the method name for <method>.
| Path | Purpose |
|---|---|
server/.golangci.yaml |
Per-file linter exceptions. |
server/cmd/gram/start.go |
Wires every service into the HTTP mux. |
server/design/<svc>/design.go |
The service's Goa design. Regenerates server/gen/<svc>/ and server/gen/http/<svc>/ via mise run gen:goa-server. |
server/design/gram.go |
Root Goa import graph. Regenerates the aggregate server/gen/** tree via mise run gen:goa-server. |
server/design/security/ |
Shared security schemes and helpers. |
server/design/shared/ |
Design types shared across services. |
server/internal/<svc>/<method>_test.go |
Black-box test file, one per method. |
server/internal/<svc>/impl.go |
The service implementation. |
server/internal/<svc>/queries.sql |
The service's SQLc queries. Regenerates server/internal/<svc>/repo/ via mise run gen:sqlc-server. |
server/internal/<svc>/setup_test.go |
Shared test harness for the service. |
server/internal/<svc>/shared.go (or similar) |
Non-trivial helpers specific to the service. |
server/internal/mv/<svc>.go |
View builders for the service's response types. |
Generated files
Files under server/gen/** and any repo/ subdirectory carry a DO NOT EDIT header.
| Path | Generator |
|---|---|
server/gen/<svc>/{service.go, client.go, endpoints.go} |
mise run gen:goa-server from server/design/<svc>/design.go. |
server/gen/http/<svc>/{server,client}/{server.go, client.go, cli.go, encode_decode.go, paths.go, types.go} |
mise run gen:goa-server from server/design/<svc>/design.go. |
server/gen/types/ |
mise run gen:goa-server — aggregates types across services that mark Meta("struct:pkg:path", "types"). |
server/internal/<svc>/repo/{db.go, models.go, queries.sql.go} |
mise run gen:sqlc-server from server/internal/<svc>/queries.sql (via the service's stanza in server/database/sqlc.yaml). |
Server-client contract
The server's design authors the contract; clients receive it through generated OpenAPI, a TypeScript SDK, and CLI bindings. The contract is versioned through changesets.
HTTP routes. /rpc/<service>.<method>. Method names are derived from the Goa Service/Method names — renaming them is a breaking change for CLI and any direct HTTP callers.
OpenAPI. server/gen/http/openapi3.yaml is the raw Goa output (Goa also emits a sibling openapi3.json, but it is gitignored — nothing consumes it). .speakeasy/workflow.yaml defines two sources (Gram-Internal, Gram-Public) that apply overlays from .speakeasy/overlays/ to produce .speakeasy/out.openapi.yaml and .speakeasy/openapi-public.yaml. The TypeScript SDK is generated from the internal spec.
TypeScript SDK. client/dashboard/src/sdk/ — per-operation modules under src/funcs/, a per-service class under src/sdk/, React Query hooks under src/react-query/, and model types under src/models/{components,operations}/.
CLI bindings. server/gen/http/cli/gram/cli.go provides command bindings over the same endpoints.
Changesets. .changeset/<kebab-slug>.md with YAML frontmatter of the form "server": minor (or "dashboard": patch, etc.) and a one-paragraph body. A new endpoint is usually "server": minor; a bug fix or dashboard-only change is patch.
Regeneration. After any design change, run mise run gen:goa-server (server stubs, OpenAPI, CLI), then mise run gen:sdk (Speakeasy overlays, TypeScript SDK, the public OpenAPI output). gen:sdk accepts --check (fail if outputs drift), --skip-versioning (no version bump during iteration), and --skip-upload-spec (skip the Speakeasy registry upload).
Jobs to be done
How to add a new method to an existing service
- Edit the service's
server/design/<svc>/design.go— add aMethod("<method>", ...)block withPayload,Result,HTTP(...), and the three OpenAPI meta keys. - Run
mise run gen:goa-serverto regenerate the service interface and HTTP server code. - Implement the new method on the service in
server/internal/<svc>/impl.go, following the handler skeleton. - Add any required SQLc queries to
server/internal/<svc>/queries.sqland runmise run gen:sqlc-server. - Add a view builder in
server/internal/mv/<svc>.goif the response introduces a new shape. - Gate the handler with an existing RBAC scope (see
gram-rbacskill) and emit audit events for mutations (seegram-audit-loggingskill). - Write a
<method>_test.gofile covering the happy path plus the RBAC-deny and not-found / bad-request paths. - Run
mise run lint:serverandmise run test:server ./internal/<svc>/.... - Run
mise run gen:sdkto propagate the change into the TypeScript SDK and OpenAPI outputs. - Add
.changeset/<kebab-slug>.mdwith"server": minor.
How to add a brand-new management API service
- Add a blank import for the new package in
server/design/gram.go(alphabetised). - Create
server/design/<svc>/design.gowith the service declaration,Security(...)calls,shared.DeclareErrorResponses(), and the initial set of methods. - Add a stanza in
server/database/sqlc.yamlpointing atserver/internal/<svc>/queries.sqland writing toserver/internal/<svc>/repo. - Add
server/internal/urn/<resource>.go(and a test file) for each new resource the service owns, following theserver/internal/urn/api_key.gotemplate. - Author
server/internal/<svc>/queries.sql. - Run
mise run gen:goa-serverandmise run gen:sqlc-server(ormise run gen:serverwhich runs both). - Implement
server/internal/<svc>/impl.go:Servicestruct, compile-time assertions,NewService,Attach,APIKeyAuth(whenByKeyis advertised), and each method handler. - Add view builders in
server/internal/mv/<svc>.go. - Wire the service into
server/cmd/gram/start.gowith<svc>.Attach(mux, <svc>.NewService(...))near the other attachments. - Add
server/internal/<svc>/setup_test.goplus one<method>_test.goper handler. - Add any new RBAC scopes (
gram-rbac) and audit subjects (gram-audit-logging) the service needs. - Run
mise run lint:server,mise run test:server, andmise run gen:sdk. - Add
.changeset/<kebab-slug>.mdwith"server": minor. - Run
mise run go:tidyif imports changed.
How to change a payload or result type
- Edit the type in
server/design/<svc>/design.go. Prefer additive changes: newAttributecalls on existing types are backwards compatible;Required("...")additions and attribute removals are not. - Run
mise run gen:goa-server. - Update the handler in
server/internal/<svc>/impl.goto populate or consume the new fields.exhaustructwill flag missed struct fields at lint time. - Update the view builder in
server/internal/mv/<svc>.goif the response type changed. - Run
mise run gen:sdkto propagate to the SDK surface, then update any dashboard consumers (seefrontendskill). - Update tests to cover the new shape.
- Add
.changeset/<kebab-slug>.md—"server": patchfor additive changes,minorfor anything consumers would notice.
How to rename an endpoint or override its SDK / hook name
SDK surface names are controlled by OpenAPI metadata, not by renaming Go symbols.
- Change
Meta("openapi:operationId", ...),x-speakeasy-name-override, and/orx-speakeasy-react-hookon theMethodin the design file. - Run
mise run gen:goa-serverthenmise run gen:sdkto regenerate the SDK and hook names. - Update dashboard consumers to use the new names (see
frontendskill). - Note that the HTTP path is derived from the Goa
Service/Methodnames — changing those is a breaking change for CLI and any direct HTTP callers. Prefer adding a new method and deprecating the old.
How to regenerate outputs after a design change
Order matters because each step's output is the next step's input.
mise run gen:goa-server— after any edit underserver/design/**.mise run gen:sqlc-server— after any edit underserver/internal/*/queries.sqlorserver/database/sqlc.yaml. Requires the local Postgres container frommise run infra:start(sqlc connects to the database to type-check queries). (Or usemise run gen:serverto run both.)mise run lint:serverandmise run test:server— catch struct-field drift early.mise run gen:sdk— regenerate the TypeScript SDK and the public/internal OpenAPI files.mise run go:tidyif imports changed.
Relevant mise tasks
| Task | Purpose |
|---|---|
mise run build:server |
Build the server binary. |
mise run build:tunnel-gateway |
Build the tunnel gateway binary. |
mise run gen:goa-server |
Regenerate everything under server/gen/** from the Goa design files. |
mise run gen:sdk |
Apply Speakeasy overlays and regenerate the TypeScript SDK and both public/internal OpenAPI files. Flags: --check, --skip-versioning, --skip-upload-spec. |
mise run gen:server |
Convenience: runs gen:sqlc-server then gen:goa-server. |
mise run gen:sqlc-server |
Regenerate every repo/ package from every queries.sql. Requires mise run infra:start (sqlc connects to the local Postgres to type-check queries). |
mise run go:tidy |
go mod tidy across the workspace. |
mise run lint:server |
golangci-lint over the server tree including exhaustruct. |
mise run test:server |
Runs go test across the server tree; accepts go test arguments. |
Maintaining this skill
This file documents conventions that evolve over time. Adding a new endpoint or service using the patterns above is already covered by "Jobs to be done" — those don't require skill edits. Structural changes do. Update this skill in the same commit when you make any of the following kinds of changes:
- Changing the
/rpc/<service>.<method>HTTP route convention, or introducing a second route convention alongside it. - Adding, removing, or renaming a security scheme (
Session,ByKey,ProjectSlug). - Changing the required OpenAPI meta keys or their semantics.
- Replacing
mv/with a different view pattern, or changing theBuild<Subject>Viewnaming convention. - Changing the
impl.goanatomy — new required struct fields, new middleware layer inAttach, a differentNewServicecontract. - Changing the black-box test convention (
package <svc>_test,testenv.Launch,newTestService) or moving shared helpers out of per-servicesetup_test.go. - Changing the Speakeasy workflow — new overlay stages, different source definitions in
.speakeasy/workflow.yaml, a third OpenAPI output alongside internal/public. - Changing the changeset format or adding a new package target.
- Adding a new API-relevant mise task that belongs on the cheat sheet.
Cross-references
golang— Go code style, error handling (oops), logging, testing, dependency injection, and theconv/o11yhelpers referenced in the handler skeleton.postgresql— schema design, migrations, SQLc query rules, and theproject_idscoping requirement.gram-rbac— scope declaration andaccess.Require(ctx, access.Check{...})enforcement inside handlers.gram-audit-logging— emittingaudit.Log*calls inside handler transactions.frontend— consuming endpoints from the dashboard via the generated SDK and React Query hooks.mise-tasks— modifying any of the.mise-tasks/gen/*.shscripts referenced above.