Appearance
Composing an API surface
An API surface is the set of /v1 operations one bounded context serves, together with the probes, workers, and dialled clients it needs. The cmd/plexsphere composition root composes the surfaces listed in ProductionSurfaces() through the surface registry: each surface implements a small contract, and Run walks the ordered list in fixed phases.
There is one way to add a surface, and this page describes it. It covers the contract, the phases, the registry order, the transport dependency struct, the opt-out rule, the backup surface as a worked example, and the gates that pin the result. The two checklists at the end are the short form. For where cmd/plexsphere sits in the repository, read the repository layout. For the test tiers the gates belong to, read the testing guide.
The surface contract
The contract lives in cmd/plexsphere/surface.go, in package main. It stays there because Runtime carries the unexported dbPools type, and no code outside the binary's own Go module uses the contract.
Runtime
Runtime is the set of shared dependencies Run hands every surface at build time. newRuntime builds it once from Config, and the registry passes it to Build by value. A surface publishes what a later surface needs through Bundle.Export, which writes onto the registry's working copy between two builds. Export is the only way one surface reaches another.
Every exported value is its own typed field rather than an entry in a type-keyed map, so a missing producer is a compile error while the registry is being written instead of a nil lookup at request time.
| Field | Type | Producer |
|---|---|---|
Logger | *slog.Logger | The boot logger of Run. It is never nil. |
Pools | *dbPools | Config.DBPools, the three-tier pool provider main.go assembles. Nil when no DSN is wired. |
SpiceDB | *authz.Client | Config.SpiceDBClient, or the client Run dialled from SPICEDB_ENDPOINT when that field is nil. Nil without ReBAC wiring. |
AuditSink | audit.Sink | The audit surface's Export, seeded from Config.AuditSink. The surface writes it only when it built a sink, so a Config seed survives an opted-out surface. |
AuthzCheck | AuthzCheckFunc | Config.AuthzCheck. main.go sets it only when PLEXSPHERE_DSN is set. |
Authorizer | *authz.Authorizer | Config.Authorizer, built over the SpiceDB client in main.go. |
Pepper | audit.Pepper | Config.AuditPepper, built once by buildAuditPepper in main.go. |
Metrics | prometheus.Registerer | Config.Metrics. main.go sets prometheus.DefaultRegisterer; a nil value leaves a surface on its own fallback. |
PlexdDownloadPin | *distribution.PinnedRelease | Config.PlexdDownloadPin. The release the plexd-download surface serves; nil selects the pin embedded from /PLEXD_VERSION and the release's checksum file. A test sets a fixture pin. |
Leader | *leader.Elector | The background-leader surface's Export, seeded from Config.BackgroundLeader. |
CapacitySSEObserver | func(domainID uuid.UUID, events int) | The capacity surface's Export, seeded from Config.CapacitySSEObserver. |
CapacitySecretReadsObserver | func(domainID string, reads int) | The capacity surface's Export, seeded from Config.CapacitySecretReadsObserver. |
CapacityIngestObserver | func(domainID string, wireBytes int) | The capacity surface's Export, seeded from Config.CapacityIngestObserver. |
SSEPublisher | *sse.Publisher | The sse surface's Export, seeded from Config.SSEPublisher. |
PolicyCompileService | authzsync.CompileService | The policy-compile surface's Export, seeded from Config.PolicyCompileService. |
PeerAnchorer | authzsync.PeerAnchorer | The peers surface's Export, seeded from Config.PeerAnchorer. The surface writes it only while it is still nil. |
NodeAuth | *nodeAuth | The node-auth surface's Export. |
Peers | *PeersWiring | The peers surface's Export. |
Labels | *LabelsWiring | The labels surface's Export. |
CredentialAssignments | *caservices.CredentialAssignmentService | The credential-assignments surface's Export. |
CloudAssignments | *caaservices.CloudAssignmentService | The cloud-assignments surface's Export. |
SinkEnablements | *sinkenablementservices.SinkEnablementService | The sink-enablements surface's Export. |
InvitationsConsumer | *invconsumer.Consumer | The invitations surface's Export. |
A field is nil when its producer opted out. What a consumer does with that nil is its own decision: it either refuses to build with its own sentinel, or opts out with the producer. The registry-order comment of the consuming entry names which of the two it is.
cmd/plexsphere/surface_runtime_integration_test.go runs under make test-integration and pins the SpiceDB row. It boots Run with SPICEDB_ENDPOINT set and Config.SpiceDBClient nil, and a surface must receive the client Run dialled.
Surface
Surface is one API surface the registry composes (doc comments omitted):
go
type Surface interface {
Name() string
LoadConfig(getenv func(string) string) error
Build(ctx context.Context, rt Runtime) (*Bundle, error)
}Nameidentifies the surface in logs, errors, and the shutdown closer stack. It is non-empty and unique across the registry.LoadConfigreads the surface config throughgetenv. An error fails the boot.Buildconstructs the surface from its loaded config and theRuntime. A nil*Bundlewith a nil error means the surface opted out.
Bundle
Bundle is what a built surface contributes to Run.
| Field | Type | Meaning |
|---|---|---|
Probes | []Probe | The readiness probes the surface registers. |
Workers | []worker.Worker | The background loops the surface registers with the worker supervisor. |
Mount | func(deps *v1.Deps) | Installs the surface handlers on the /v1 router dependencies. It is nil for a surface without HTTP operations. |
MountRoot | func(r chi.Router) | Installs the surface handlers on the root router, outside /v1. It is nil for a surface without root routes. Run calls every MountRoot before it mounts /v1, in every build, including the e2e_no_v1 build whose /v1 mount is a no-op. |
Close | func() error | Releases what the surface dialled. It is nil when the surface dialled nothing. |
Export | func(rt *Runtime) | Publishes the values this surface produces onto the registry's working Runtime, so the surfaces built after it read them. It is nil for a surface that produces nothing for another surface. |
build calls Export once, after the bundle is built and before the next surface builds.
Probe
Probe is one readiness probe a surface wants registered.
| Field | Type | Meaning |
|---|---|---|
Name | string | Identifies the probe in registration errors. It must not be empty. |
Register | func(ctx context.Context, registry *health.Registry) error | Adds the probe to the boot registry. It receives the registry itself, so a surface can call any bootstrap.Register*Probe helper or health.Registry.Register directly. It must not be nil. |
NewSurface
NewSurface binds a typed config loader and a builder into a Surface, so a surface is two functions and one NewSurface call:
go
func NewSurface[C any](
name string,
load func(getenv func(string) string) (C, error),
build func(ctx context.Context, rt Runtime, cfg C) (*Bundle, error),
) SurfaceC is the config type of the surface. The returned value keeps the config load returned and hands it to build. LoadConfig returns a loader error unchanged, so the registry adds the surface name exactly once. Build before a successful LoadConfig returns ErrSurfaceNotLoaded.
NewSurface panics on an empty name, a nil load, or a nil build. A registry entry is a compile-time literal, so a bad one is a programmer error that fails the boot.
Sentinels
Callers branch on the four registry sentinels with errors.Is.
| Sentinel | Returned by | Condition |
|---|---|---|
ErrSurfaceNameEmpty | newSurfaceRegistry | A surface has an empty Name. |
ErrSurfaceNameDuplicate | newSurfaceRegistry | An earlier surface already uses the Name. |
ErrSurfaceNotLoaded | Build of a NewSurface value | LoadConfig has not succeeded. |
ErrSurfaceProbeInvalid | registerSurfaceProbes | A probe has an empty Name or a nil Register. |
The registry phases
cmd/plexsphere/registry.go walks the ordered surface list in phases. Every phase keeps the list order, so boot logs and errors read in registry order.
| Phase | Function | What it does | Failure |
|---|---|---|---|
| New | newSurfaceRegistry(surfaces) | Validates the names. A nil or empty slice yields a registry that loads, builds, and registers nothing. | ErrSurfaceNameEmpty or ErrSurfaceNameDuplicate. |
| Load | (*surfaceRegistry).load(getenv) | Calls LoadConfig on every surface. | The first error stops the walk and is returned wrapped with the surface name. |
| Build and export | (*surfaceRegistry).build(ctx, rt, closers) | Calls Build on every surface. A nil bundle logs api surface disabled and leaves the surface out. A bundle pushes its Close onto the closer stack as surface:<name> at once, logs api surface enabled, and then runs its Export against the working Runtime. Both log lines are INFO with a surface attribute. The second return value is that Runtime, returned on every path including the error path. | The first error stops the walk and is returned wrapped with the surface name. |
| Probes | registerSurfaceProbes(ctx, registry, built) | Registers every probe of every built surface on the boot health registry. | ErrSurfaceProbeInvalid, or the Register error (such as a probe name another surface already registered) wrapped with the surface and probe names. |
| Workers | registerSurfaceWorkers(sup, built) | Registers every worker of every built surface with the worker supervisor. | The supervisor error wrapped with the surface and worker names. The worker sentinels (ErrEmptyName, ErrNilRun, ErrDuplicateName, ErrAlreadyStarted) still match with errors.Is. |
| Mount | mountSurfaces(&deps, built) | Calls every non-nil Mount hook with the v1.Deps value. | Returns no error. A nil deps is a composition-root defect and panics. |
| Close | (*closerStack).closeAll | Calls every pushed Close at shutdown, newest first. | A failing Close logs shared resource close failed at WARN with a resource attribute, and the teardown continues. |
Where the phases sit in the boot
Run in cmd/plexsphere/app.go carries the authoritative boot order in one DECISION block, and every other ordering comment in the composition root points at it. The sequence is:
bootstrap.Bootstrapbuilds the boot logger, the health registry, and the router, and runs the migrations. The teardown defer and the closer stack are in place before anything is pushed onto them. Right after themRunregisters thebootreadiness probe, builds the rate-limit middleware, binds the API's address, and serves the boot gate, abootGateoverhealth.NewBootHandler, behind the rate-limit middleware and the correlation middleware outside it:/livezanswers200,/readyzanswers503withbootfailing, and every other request answers503until step 9.- The audit-pepper closer and its
/readyzprobe. - The SpiceDB dial, the readiness probe over
*authz.Client.Ping, and the synchronous schema apply, which waits while SpiceDB is unreachable. - The shared pool closer and the boot-latched
db-primaryandjetstreamprobes. - The registry:
newSurfaceRegistry,load, andbuildwithnewRuntime(cfg, boot.Logger, spicedbClient), then the probes phase and the workers phase.buildhands back the built surfaces and the finalRuntime, which carries every export. - The deprecation table from the embedded OpenAPI document, the
workersprobe, andsup.Start. Runcomposes the basev1.Depsit owns (Registry,Build,Authn,CSRF,Deprecation) and hands it tomountV1together with the built surfaces.mountV1callsmountSurfaces(&deps, surfaces), checks the result withdeps.Validate(), and then callsv1.Mount(r, deps). The transport dependency struct lists whatValidaterejects.- The NSK wrappers around
boot.Router, built from theNodeAuthvalue on the runtimebuildreturned. A nil value leaves the router unwrapped. - The gate switches to the NSK-wrapped router, so the rate-limit and correlation middlewares around the gate front it, and
Runwrites the addr file. - At shutdown the server-stop defer stops the server and releases the address. The teardown defer then cancels and joins the workers, and closes the shared resources, surface
Closefunctions included, newest first.
Five constraints fix that order:
The address is bound right after bootstrap, so
/livezanswers for as long as the boot takes, and the boot gate serves no other request until it switches to the API routes.Migrations run inside
bootstrap.Bootstrap, so a missing table surfaces as a/readyzprobe failure rather than a per-request 500.Every probe a surface owns is registered before the supervisor starts, because a loop such as the reachability evaluator has to observe a fully wired ledger on its first tick.
Every worker a surface owns is registered before the supervisor starts, because
worker.Supervisor.Registerrefuses a registration onceStartran.The registry builds before the mount, because the mount consumes the built surfaces, and the NSK wrapper runs after the build, because it reads the runtime the build returns.
A failing phase returns an error that already names the surface, and Run returns it as is. The boot fails before the API serves its routes, and the server stops before Run returns, so a failed boot leaves nothing listening. The teardown defer is registered before anything is pushed onto the closer stack, so the bundles that built before the failure are closed on the way out.
The registry order
cmd/plexsphere/surfaces_prod.go holds the list. Its slice order is the build order, the export order, the probe-registration order, and the mount order at once. An entry whose position is load-bearing carries a comment naming the constraint that fixes it; the rest say that the position preserves the order the composition root already had.
Three kinds of constraint appear:
- A data dependency the builder refuses to go without. The
domainssurface returnsErrDomainsAuditSinkRequiredon a runtime whoseAuditSinkis nil, so it is listed afteraudit. A wrong order here fails the boot with the sentinel. - An export a later surface reads to decide whether it runs at all. The four NSK-gated surfaces opt out on a nil
Runtime.NodeAuth, so anode-authentry listed after them would disable all four with no error at all. - A
v1.Depsfield aMounthook writes only while it is still nil. Thestate,node-auth, andreachabilitysurfaces each carry aNodeRepoadapter and each writes it into a nil field only, so the list order alone decides which one supplies the port. Theauthsurface writesAuthnandCSRFthe same way, so aConfig.AuthnMiddlewareorConfig.CSRFMiddlewarea test stamps survives the mount.
TestProductionSurfaces_Order in cmd/plexsphere/surfaces_prod_test.go pins the list. It asserts every dependency as a position comparison against a named reason rather than against a frozen name list, so an insertion that violates no dependency stays green, and one that does names both surfaces and the value at stake. The surface count is pinned separately, because a list that lost most of its entries would satisfy every comparison.
The node-auth surface
cmd/plexsphere/node_auth_surface.go builds the per-Node NSK authentication once per boot: the resolver over plexsphere.node_secret_key, the middleware composed from it, the Node read port, and the audit sink the NSK-gated handlers record outcomes through. Its Export publishes the four on Runtime.NodeAuth. Its Mount hook writes NSKResolver and AuditSink onto v1.Deps, and NodeRepo when the state surface supplied none.
heartbeat, capabilities, integrity-violations, and observability-ingest read the wiring off the runtime and opt out when it is nil. Run reads it too: it wraps boot.Router with the shared per-Node middleware, then with the no-path-id authenticator for POST /v1/keys/rotate and the two passthrough authenticators the dual-credential reads need. The NSK path grammar and those three wrappers live in cmd/plexsphere/node_auth_routing.go.
An empty PLEXSPHERE_DSN opts the surface out. The four gated surfaces opt out with it, and the router stays unwrapped, so every /v1 request runs through the cookie and JWT chain alone.
TestProductionSurfaces_NodeAuthOrder pins that the four gated surfaces are registered after node-auth.
The transport dependency struct
v1.Deps is the struct v1.Mount hands to the /v1 handlers. It is an alias of handlers.Deps, declared in internal/transport/http/v1/handlers/deps.go. The struct lives in the handlers package because the port interfaces live there, and the alias keeps v1 the only package a composition root imports. It has one field per port or per-context bundle the handlers read, plus the Authn, Authz, CSRF, and Deprecation middleware fields that only v1.Mount reads.
A surface adds one field to the struct, and its dispatch file reads the field as h.deps.<Field>. A nil field keeps the operations on their 501 stub:
go
if h.deps.Backup == nil {
backupNotProvisioned(w, r, "GetBackupCatalog")
return
}The field needs no setter and no forwarding code. v1.Mount passes the struct to handlers.New. A surface writes the field from its Mount hook on the *v1.Deps it receives.
Validation
Deps.Validate reports every same-origin port group that is only partly wired. Both fields of such a group are written by one surface Mount hook, so a set with one member and not the other is always a composition defect.
| Set field | Required field | Origin surface |
|---|---|---|
AuditQueryService | AuditAuthorizer | audit (cmd/plexsphere/audit_factory_prod.go) |
AuditChainStore | AuditAuthorizer | audit |
AuditEraseService | AuditAuthorizer | audit |
AuditEraseService | AuditPepper | audit |
NodeListReader | NodeListAuthzChecker | nodes-list (cmd/plexsphere/nodes_list_prod.go) |
EventStream | SignatureVerifier | sse (cmd/plexsphere/sse_factory_prod.go) |
EndpointRecorder | PeerLookup | peers (cmd/plexsphere/peers_factory_prod.go) |
Each violated row yields one error that wraps v1.ErrDepsIncomplete and names both fields, such as EventStream is set but SignatureVerifier is nil. Validate joins them with errors.Join, so errors.Is(err, v1.ErrDepsIncomplete) matches the result. A nil result means the struct is mountable. An unwired group is valid and keeps its operations on the 501 stub. A new port group whose two fields come from one surface adds its row to the depsPortGroups table in deps.go.
Companions from different surfaces are not checked. RelationChecker comes from mesh, NSKResolver from node-auth, NodeRepo from whichever of state, node-auth, and reachability supplies it first, and PeerLookup also guards the rotate operations. A deployment without the mesh or the per-Node authentication is a supported posture, so those operations keep their fail-closed 501 gates and the boot goes on. Validate does not look inside bundle pointers such as Backup: a non-nil bundle with nil inner ports is that bundle's own concern.
v1.Mount does not call Validate, because tests mount partial sets on purpose to pin the 501 gates. mountV1 validates once, after mountSurfaces ran, so a field a surface Mount hook writes is checked too.
Where the deps are pinned
Every port reaches v1.Deps through a surface Mount hook, and the per-surface Test…Bundle_MountWritesDeps unit tests pin those writes. TestRun_IncompleteDeps_FailsBoot in cmd/plexsphere/mount_v1_test.go pins the other half at the Run level: a bundle that writes one member of a port group fails the boot with the Validate error, and the address file is never written.
Opting out
A surface opts out when Build returns a nil bundle and a nil error. The registry logs api surface disabled with the surface name and leaves the surface out of every later phase: no probe, no worker, no mount hook, no Export, and no Close. The operations of the surface keep dispatching to their 501 stub, and the not-provisioned problem code they answer stays unchanged. An opt-out is not an error. Only a non-nil error from Build fails the boot.
Two Config fields decide what the registry sees:
Config.Surfacesis the ordered listRuncomposes. Nil selectsProductionSurfaces(). A non-nil empty slice registers nothing, which is what a test passes to isolate a boot from every registered surface. Nil selects the production list because the alternative,main.goassigning the list, fails silently: one dropped assignment would leave every registered surface answering its stub.Config.AuthzCheckreaches the surfaces asRuntime.AuthzCheck. Nil makes the ReBAC-gated surfaces opt out.main.gosets it only whenPLEXSPHERE_DSNis set, behind the same DSN check the surface loaders apply.
Worked example: the backup surface
cmd/plexsphere/backup_surface.go serves GET /v1/platform/backup/catalog and GET /v1/platform/backup/restore-plan. Both reads are platform-scoped projections of the build-time catalog in internal/platform/backup, gated by the ReBAC manage check on the platform anchor. The surface has no database, no worker, and no environment knob.
Load the config
The backup surface has no knob, so its config type is empty and its loader never fails (doc comments omitted):
go
type backupSurfaceConfig struct{}
func loadBackupSurfaceConfig(_ func(string) string) (backupSurfaceConfig, error) {
return backupSurfaceConfig{}, nil
}A surface with knobs reads each one through getenv and returns an error for a malformed value, which fails the boot.
Build the bundle
buildBackupSurface ends in one of three ways (excerpt, the audit sink adapter and the transport dependencies elided):
go
if rt.AuthzCheck == nil {
return nil, nil
}
cat := backupCatalog()
if err := cat.Validate(); err != nil {
return nil, fmt.Errorf("validate backup catalog: %w", err)
}
return &Bundle{Mount: func(d *v1.Deps) { d.Backup = &deps }}, nilThese are the three outcomes a surface has:
- A nil
rt.AuthzCheckopts the surface out. Both reads keep answering 501 with thebackup_not_provisionedproblem code. - A catalog that fails validation returns an error. The registry wraps it with the surface name, and the boot fails.
- Otherwise the bundle carries only a
Mounthook, which setsv1.Deps.Backup. The surface dials nothing, soClosestays nil, and it produces nothing for another surface, soExportstays nil too.
backupCatalog is a package variable that points at platformbackup.Default. A unit test swaps it to inject an invalid catalog.
Register the surface
backupSurface binds the loader and the builder under the name backup with NewSurface. Each call returns a fresh value, so two registries never share a loaded config:
go
func backupSurface() Surface {
return NewSurface("backup", loadBackupSurfaceConfig, buildBackupSurface)
}ProductionSurfaces() lists that constructor. A new surface adds its constructor to the slice, after every surface whose exports it reads and before every surface that reads its own.
The unit tests in cmd/plexsphere/backup_surface_test.go cover the opt-out on a nil AuthzCheck, the Mount hook, the audit forward of a denial, the wrapped catalog validation error, and the empty config load. A new surface carries the same set for its own outcomes.
The builder behind a surface
The backup surface builds its bundle inline because it dials nothing. A surface backed by Postgres or another external dependency keeps four symbols in its own <ctx>_factory_prod.go:
production<Ctx>ConfigFromEnv(getenv)is the loader. It reads the surface knobs and returns the typed config, or an error that fails the boot.BuildProduction<Ctx>Factory(cfg)validates the config once and returns the builder closure, or a nil closure when the config opts the surface out.func (w *<Ctx>Wiring) bundle(rt Runtime) *Bundleturns the assembled graph into theBundle: the probes, the workers, theMounthook, theClose, and theExport.func <ctx>Surface() Surfacebinds the loader and the build function withNewSurfaceunder the surface name. This is the symbolProductionSurfaces()lists.
The surface's build function stamps the shared values off the Runtime onto its config (cfg.Logger = rt.Logger, cfg.AuthzCheck = rt.AuthzCheck, cfg.Pepper = rt.Pepper), asks for the factory, invokes it, and returns w.bundle(rt). A nil factory or a nil wiring opts the surface out. Nothing reaches a builder off Config directly: the pool provider, the chained audit sink, the elector, the capacity observers, and the SSE publisher all come off rt, which is what makes registry order the only thing that decides what a builder sees.
The gates
Unit tier
cmd/plexsphere/surface_registry_gate_test.go is the compiled gate for the registered surfaces. Four tables carry it: surfaceOperations lists the operations each surface mounts, surfaceNotProvisionedCodes the problem code those operations answer while the surface is opted out, infraFreeSurfaces the surfaces that build in a boot with no external infrastructure, and alwaysMounted the operations the router serves outside every surface.
TestProductionSurfaces_EveryOperationDispatchesfails when a registered surface has nosurfaceOperationsentry or when an entry names no registered surface. It then bootsRunwith anAuthzCheckthat admits every check and fails for every listed operation of an infrastructure-free surface that still answers 501.TestProductionSurfaces_OptOutKeepsNotProvisionedbootsRunwith an emptyConfig, so every gated surface opts out. Every listed operation must answer 501 with anapplication/problem+jsonbody carrying the problem code fromsurfaceNotProvisionedCodes. A surface without an entry in that table fails.TestSurfaceTableDrift_DetectsMissingAndStaleEntriespins the drift check itself: a table in step, a registered surface missing from the table, a stale entry, and both at once.TestSurfaceOperations_CoverTheSpecparses the embedded/v1OpenAPI document and requires every operation it declares to be listed under exactly one surface, or underalwaysMounted. An entry that matches no path of the document fails the same test.
cmd/plexsphere/surfaces_prod_test.go pins the list itself: TestProductionSurfaces_Order for the count, the uniqueness, the first entry, and every producer-before-consumer comparison, TestProductionSurfaces_NodeAuthOrder for the four NSK-gated surfaces, and TestProductionSurfaces_ReturnsFreshSurfaces for the per-call freshness a loaded config depends on.
The per-surface Test…Bundle_MountWritesDeps tests pin which v1.Deps fields each Mount hook writes.
Integration tier
cmd/plexsphere/surfaces_prod_postgres_integration_test.go boots Run with the production registry over a real Postgres and a real SpiceDB, assembled the way main.go assembles it. TestRun_ProductionSurfaces_DispatchOverPostgres proves what the unit tier cannot: that a listed surface builds. Every surface the boot keeps must log api surface enabled, and every operation of the embedded OpenAPI document must answer something other than 501. Three lists carve out what the boot does not cover, and each entry names the infrastructure it lacks or the port the composition root leaves unassigned. A baseline boot with no configuration runs first, so an operation the generated wrapper rejects before dispatch cannot pass the sweep while asserting nothing.
cmd/plexsphere/surfaces_prod_integration_test.go boots the same registry with an AuthzCheck over a real SpiceDB that carries the canonical schema and no relationship tuples, and TestRun_ProductionSurfaces_BackupDeniesOverRealSpiceDB requires the catalog read to answer 403.
End to end
The assert-surfaces-mounted step of the bootstrap-seed scenario in tests/e2e/bootstrap-seed/chainsaw-test.yaml runs against the deployed binary. An anonymous GET /v1/platform/backup/catalog must answer 401 rather than 501, and the container's boot log must put every registered surface in the state that manifest's environment configures, matched line by line against the api surface enabled and api surface disabled records.
Add a surface
- Declare the surface config type and its loader
production<Ctx>ConfigFromEnv(getenv)incmd/plexsphere/<ctx>_factory_prod.go. Return an error for a malformed value and the opt-out for an unset one. - Declare
<Ctx>Wiringwith the ports and loops the surface produces, andBuildProduction<Ctx>Factory(cfg)returning the builder closure. - Write
func (w *<Ctx>Wiring) bundle(rt Runtime) *Bundle. FillProbes,Workers,Mount,MountRoot,Close, andExportwith what the surface actually contributes, and leave the rest nil.MountRootis for the rare route outside the versioned/v1contract, such as the/downloads/plexdpath a booting machine fetches its agent from; such a route also needs asurfaceRootOperationsentry and aPathPrefixon the API HTTPRoute of the chart and the dev stack. - Write the build function that stamps the shared values off
rt, invokes the factory, and returnsw.bundle(rt)or the opt-out. - Add
func <ctx>Surface() Surfacewith theNewSurfacecall and the surface name. - Add the field the handlers read to
handlers.Depsininternal/transport/http/v1/handlers/deps.go, and write it from theMounthook. Add adepsPortGroupsrow when the surface writes a same-origin port pair. - Append the constructor to
ProductionSurfaces()incmd/plexsphere/surfaces_prod.go. Place it after every surface whose exports it reads, and add the comment naming that constraint. - When the surface exports a value, add the typed field to
Runtimeincmd/plexsphere/surface.goand its row to the table above. - Add the
surfaceOperationsentry and, when the surface mounts an operation, thesurfaceNotProvisionedCodesentry. Add the order constraint toproductionSurfaceOrderConstraintsand raiseproductionSurfaceCountincmd/plexsphere/surfaces_prod_test.go. - Write the unit tests for the loader, the opt-out, each build failure, and the
Mounthook, plus the integration and e2e coverage the feature needs. - Update this page and the context documentation.
Add an operation to a surface
- Add the path and the
operationIdtoapi/openapi/plexsphere-v1.yaml, then runmake generate. - Implement
func (h *Handlers) <Op>in the matchinginternal/transport/http/v1/handlers/<ctx>_dispatch.go, gated on theDepsfield the surface writes, with the not-provisioned fallthrough. - Put the request decoding, the authz check, and the error mapping in
internal/transport/http/v1/<ctx>/. - Extend the surface's wiring and its
Mounthook when the operation needs a port the surface does not carry yet. - Add the operation to the surface's
surfaceOperationsentry incmd/plexsphere/surface_registry_gate_test.go.TestSurfaceOperations_CoverTheSpecfails until you do. - Update the per-tag spec gate, the API reference page, and the tests for the three tiers.