Documentation
¶
Overview ¶
Package mirrorstack is the Go SDK for building modules on MirrorStack.
Use Init() + Start() for the convenience API, or New() for testing and advanced use.
Package mirrorstack is the Go SDK for building modules on MirrorStack.
Use Init() + Start() for the convenience API, or New() for testing and advanced use.
Package mirrorstack is the Go SDK for building modules on MirrorStack.
Use Init() + Start() for the convenience API, or New() for testing and advanced use.
Package mirrorstack is the Go SDK for building modules on MirrorStack.
Use Init() + Start() for the convenience API, or New() for testing and advanced use.
Package mirrorstack is the Go SDK for building modules on MirrorStack.
Use Init() + Start() for the convenience API, or New() for testing and advanced use.
Package mirrorstack is the Go SDK for building modules on MirrorStack.
Use Init() + Start() for the convenience API, or New() for testing and advanced use.
Package mirrorstack is the Go SDK for building modules on MirrorStack.
Use Init() + Start() for the convenience API, or New() for testing and advanced use.
Package mirrorstack is the Go SDK for building modules on MirrorStack.
Use Init() + Start() for the convenience API, or New() for testing and advanced use.
Index ¶
- Constants
- Variables
- func AbsorbInfra()
- func AppID(ctx context.Context) string
- func AppRole(ctx context.Context) string
- func Cache(ctx context.Context) (cache.Cacher, func(), error)
- func Call(ctx context.Context, targetModuleID, method, path string, body, out any) error
- func CallDependencyPost(ctx context.Context, producerRef, path string, body, out any) error
- func CallGet(ctx context.Context, targetModuleID, path string, out any) error
- func CallPost(ctx context.Context, targetModuleID, path string, body, out any) error
- func CallWithOptions(ctx context.Context, targetModuleID, method, path string, body, out any, ...) error
- func CancelTask(ctx context.Context, jobID string) error
- func ContributesTo(hostSpec, slot string, payload any)
- func Contributions() []contributions.SlotInfo
- func Cron(name, schedule string, handler http.HandlerFunc)
- func DB(ctx context.Context) (db.Querier, func(), error)
- func DeliveryURL(ctx context.Context, key string, ttl time.Duration) (string, error)
- func DependsOn(spec string, configure ...func(*Need))
- func DrainAudit(ctx context.Context) error
- func Emit(ctx context.Context, name string, payload any) error
- func Emits(name string)
- func ExposeTable(name string)
- func Init(cfg Config) error
- func Internal(fn func(r chi.Router))
- func IsPermanent(err error) bool
- func LoggerFrom(ctx context.Context) *slog.Logger
- func MCPResource[Out any](name, description string, handler func(ctx context.Context) (Out, error))deprecated
- func MCPTool[In, Out any](name, description string, ...)
- func Meter(name string, opts ...meter.MetricOption)
- func ModuleDB(ctx context.Context) (db.Querier, func(), error)
- func ModuleTx(ctx context.Context, fn func(q db.Querier) error) error
- func NewContributionSlot[T any](key string) contributions.Slot
- func Notify(ctx context.Context, n Notification) error
- func OnEvent(name string, handler http.HandlerFunc, opts ...OnEventOption)
- func OnTask(name string, handler TaskHandler, opts ...TaskOption)
- func Permanent(err error) error
- func Platform(fn func(r chi.Router))
- func Provide[T any](key string)
- func Public(fn func(r chi.Router))
- func Record(ctx context.Context, name string, value float64) error
- func RecordObservation(ctx context.Context, name string, value float64, observation meter.Observation) error
- func RecordWithID(ctx context.Context, eventID, name string, value float64) error
- func RegisterMessages(fsys fs.FS, dir string) error
- func RegisterPermission(name string, opts PermissionOpts)
- func RegisterUI(ui ModuleUI)
- func RequirePermission(name string) func(http.Handler) http.Handler
- func RequireStorage()
- func Resolve[T any](id string) (T, bool)
- func RunTask(ctx context.Context, name string, payload json.RawMessage) (string, error)
- func RunTaskWithIdempotencyKey(ctx context.Context, name string, payload json.RawMessage, ...) (string, error)
- func Start() error
- func Storage(ctx context.Context) (storage.Storer, error)
- func StoredContributions(ctx context.Context, slot string) ([]contributions.Contribution, error)
- func Tx(ctx context.Context, fn func(q db.Querier) error) error
- func UserID(ctx context.Context) string
- type CallOptions
- type Compute
- type Config
- type DeliveryTicket
- type Dependency
- type DependencyQuery
- func (q *DependencyQuery) Columns(cols ...string) *DependencyQuery
- func (q *DependencyQuery) Limit(n int) *DependencyQuery
- func (q *DependencyQuery) Result(ctx context.Context) (*DependencyResult, error)
- func (q *DependencyQuery) Rows(ctx context.Context) ([]map[string]any, error)
- func (q *DependencyQuery) Where(col string, value any) *DependencyQuery
- func (q *DependencyQuery) WhereIn(col string, values ...any) *DependencyQuery
- type DependencyResult
- type DynamicSelect
- type Label
- type MCPToolOption
- type MemberAssertion
- type Module
- func (m *Module) AbsorbInfra()
- func (m *Module) Cache(ctx context.Context) (cache.Cacher, func(), error)
- func (m *Module) Call(ctx context.Context, targetModuleID, method, path string, body, out any) error
- func (m *Module) CallDependency(ctx context.Context, producerRef, method, path string, body, out any) error
- func (m *Module) CallDependencyPost(ctx context.Context, producerRef, path string, body, out any) error
- func (m *Module) CallGet(ctx context.Context, targetModuleID, path string, out any) error
- func (m *Module) CallPost(ctx context.Context, targetModuleID, path string, body, out any) error
- func (m *Module) CallWithOptions(ctx context.Context, targetModuleID, method, path string, body, out any, ...) error
- func (m *Module) CancelTask(ctx context.Context, jobID string) error
- func (m *Module) Close()
- func (m *Module) Config() Config
- func (m *Module) ContributesTo(hostSpec, slot string, payload any)
- func (m *Module) Cron(name, schedule string, handler http.HandlerFunc)
- func (m *Module) DB(ctx context.Context) (db.Querier, func(), error)
- func (m *Module) DependencyDB(ctx context.Context, producerRef string) *Dependency
- func (m *Module) DependsOn(spec string, configure ...func(*Need))
- func (m *Module) DrainAudit(ctx context.Context) error
- func (m *Module) Emit(ctx context.Context, name string, payload any) error
- func (m *Module) Emits(name string)
- func (m *Module) ExposeTable(name string)
- func (m *Module) Internal(fn func(r chi.Router))
- func (m *Module) Meter(name string, opts ...meter.MetricOption)
- func (m *Module) ModuleDB(ctx context.Context) (db.Querier, func(), error)
- func (m *Module) ModuleTx(ctx context.Context, fn func(q db.Querier) error) error
- func (m *Module) Notify(ctx context.Context, n Notification) error
- func (m *Module) OnEvent(name string, handler http.HandlerFunc, opts ...OnEventOption)
- func (m *Module) OnTask(name string, handler TaskHandler, opts ...TaskOption)
- func (m *Module) Platform(fn func(r chi.Router))
- func (m *Module) ProvideSlot(slot contributions.Slot)
- func (m *Module) Public(fn func(r chi.Router))
- func (m *Module) Record(ctx context.Context, name string, value float64) error
- func (m *Module) RecordObservation(ctx context.Context, name string, value float64, observation meter.Observation) error
- func (m *Module) RecordWithID(ctx context.Context, eventID, name string, value float64) error
- func (m *Module) RegisterPermission(name string, opts PermissionOpts)
- func (m *Module) RegisterUI(ui ModuleUI)
- func (m *Module) RequirePermission(name string) func(http.Handler) http.Handler
- func (m *Module) RequireStorage()
- func (m *Module) Router() *chi.Mux
- func (m *Module) RunTask(ctx context.Context, name string, payload json.RawMessage) (string, error)
- func (m *Module) RunTaskWithIdempotencyKey(ctx context.Context, name string, payload json.RawMessage, ...) (string, error)
- func (m *Module) Start() error
- func (m *Module) Storage(ctx context.Context) (storage.Storer, error)
- func (m *Module) StoredContributions(ctx context.Context, slot string) ([]contributions.Contribution, error)
- func (m *Module) TaskStatus(ctx context.Context, jobID string) (TaskJobStatus, error)
- func (m *Module) Tx(ctx context.Context, fn func(q db.Querier) error) error
- type ModuleUI
- type Need
- type Notification
- type NotifyAudience
- type OnEventOption
- type PermanentTaskError
- type PermissionOpts
- type Res
- type SelectFilter
- type TaskHandler
- type TaskJobStatus
- type TaskOption
- type UIComponent
- type UIPage
- type UIProp
Constants ¶
const ( MiB int64 = 1024 * 1024 GiB = 1024 * MiB )
Binary size constants accepted by WithEphemeralStorage.
const ( UISurfaceMain = registry.UISurfaceMain UISurfaceSettings = registry.UISurfaceSettings )
Known UIPage.Surface values, re-exported from registry so module authors set ms.UISurfaceSettings rather than typing the literal.
const GatedKeyPrefix = "_gated/"
GatedKeyPrefix is the module-relative key prefix that opts an object into token gating. A module writes gated objects under it and reads them back at the same key, so the key it stores and the key it later serves are identical.
The edge's rule is broader than this (any "/"-delimited segment equal to "_gated" is gated) while the platform only ever MINTS for keys of the exact apps/<app>/<module>/_gated/… shape. That asymmetry is deliberate and fails closed: a creatively-nested key becomes unreachable, never public.
Variables ¶
var ( // read (401) — unknown app, module not resolvable in the app, no live // tunnel session, or a wrong/unbound service secret. Deliberately one // collapsed verdict; re-establish the tunnel session, don't retry blind. ErrDependencyUnauthorized = errors.New("mirrorstack: dependency read unauthorized") // ErrNotExposed: authorization failed closed (403 read_not_authorized) — // the table is not exposed by the version the producer actually runs, // there is no consent row, or the consumer install carries no DB role. // The cases are deliberately indistinguishable on the wire. ErrNotExposed = errors.New("mirrorstack: table is not exposed to this module") // relation is not readable right now (403 dependency_unavailable — // producer yanked/rolled back, grant revoked), or the platform's read // proxy is not enabled at all. Never surfaced as empty rows. ErrDependencyUnavailable = errors.New("mirrorstack: dependency unavailable") // ErrProducerNotFound: the producer ref does not resolve to an install // in this app (404 producer_not_found). ErrProducerNotFound = errors.New("mirrorstack: producer module not found in this app") )
Sentinel errors mapping the read proxy's wire error codes onto the decision-17 failure modes. Match with errors.Is; the returned errors wrap these plus the platform's human message.
var ErrCallResponseTooLarge = errors.New("ms.Call: response exceeds configured limit")
ErrCallResponseTooLarge is returned when a successful inter-module response exceeds CallOptions.MaxResponseBytes. Use errors.Is to match it.
Functions ¶
func AbsorbInfra ¶
func AbsorbInfra()
AbsorbInfra declares that this module passes EVERY platform infrastructure metric through to the app owner at 0. See Module.AbsorbInfra.
func AppID ¶
AppID returns the app id from the request context's auth identity, or "" if no identity is set. It is the inbound twin of WithAppID and the single unspoofable way a handler reads its own app: the SDK promotes the trusted, dispatch-injected app id into the identity (Platform via PlatformAuth, Public via the proxy guard's success path; Lambda via runtime.InjectResources). Reading request data (query/body) for the app id instead is forgeable.
Not module-bound (no *Module receiver) — identity lives on the context, so this works before Init and in tests.
func AppRole ¶
AppRole returns the app role from the request context's auth identity, or "" if no identity is set. Same promotion paths and context-not-module shape as AppID; "" is legitimate on surfaces with no user (internal/system/cron/ task invocations, anonymous Public requests).
func Call ¶
Call makes one inter-module hop through the platform dispatch on the default module. Panics before Init.
func CallDependencyPost ¶
CallDependencyPost addresses a declared dependency by its stable ref.
func CallWithOptions ¶
func CallWithOptions(ctx context.Context, targetModuleID, method, path string, body, out any, options CallOptions) error
CallWithOptions makes one inter-module hop with opt-in bounded/strict response decoding on the default module. Panics before Init.
func CancelTask ¶
CancelTask requests local or managed cancellation on the default Module.
func ContributesTo ¶
ContributesTo declares an outbound contribution on the default module. Panics if Init has not been called. See Module.ContributesTo.
func Contributions ¶
func Contributions() []contributions.SlotInfo
Contributions returns every contribution slot the default module has declared. Surfaced for the manifest builder + tests.
func Cron ¶
func Cron(name, schedule string, handler http.HandlerFunc)
Cron registers a cron job on the default Module created by Init(). Panics before Init — matches Platform/Public/Internal.
func DeliveryURL ¶
DeliveryURL returns the CDN delivery URL for ONE gated object, with a freshly minted credential attached. key is relative to this module's storage prefix and must live under GatedKeyPrefix.
It mints per call, scoped to the key's own directory. For many keys under one prefix — a playlist's worth of segments — call Delivery once and use the returned ticket, which is one platform round trip instead of hundreds.
🔴 An error means NO URL. It never degrades to the unsigned CDN URL and never falls back to a presigned S3 URL: an unsigned URL for gated content is a paywall bypass that looks like a working feature.
func DependsOn ¶
DependsOn declares a required dependency on the default module. Panics if Init has not been called. See Module.DependsOn for spec syntax and the Need-callback contract.
func DrainAudit ¶
DrainAudit forwards one bounded audit-outbox batch for the default module.
func Emit ¶
Emit publishes an event on the default module created by Init(). Panics before Init — matches Call/CallGet/CallPost.
func Emits ¶
func Emits(name string)
Emits declares an emitted event on the default Module. Panics before Init.
func ExposeTable ¶
func ExposeTable(name string)
ExposeTable declares an exposed table on the default Module created by Init(). Panics before Init — matches Platform/Public/Internal/Emits.
func Init ¶
Init creates the default module.
ms.Init(ms.Config{ID: "media", Name: "Media", Icon: "perm_media"})
ms.Platform(func(r chi.Router) { ... })
ms.Start()
func IsPermanent ¶
IsPermanent reports whether err contains a PermanentTaskError wrapper.
func LoggerFrom ¶
LoggerFrom returns the per-request logger attached by requestLogMiddleware, or the process default if called outside a request. Exposed as ms.Log.
func MCPResource
deprecated
MCPResource registers an agent-readable resource on the default module. The handler returns current content on demand. Output schema is derived from Out. Panics before Init or on schema derivation failure.
Deprecated: nothing can reach an MCP resource. The dispatch MCP server answers `method not found` to every resources/* call — its v1 capability set is tools only, by design (api-platform, MCPServerHandler's JSON-RPC switch), and the SDK never advertises a resources capability either. A registered resource is therefore served on the module's own router and read by nobody: adoption across the fleet is 0 of 11 modules, which is the expected outcome for a surface with no client.
Expose the same data as an MCPTool with no arguments. A tool is reachable, it carries the same derived output schema, and it can be annotated read-only (see the Tool* options) so an agent treats it exactly as it would a resource.
This will be removed once the last caller is gone. It is not removed now because doing so is a breaking change to a published module API, and there is no evidence anyone is calling it — only evidence that it would not work if they did.
func MCPTool ¶
func MCPTool[In, Out any](name, description string, handler func(ctx context.Context, args In) (Out, error), opts ...MCPToolOption)
MCPTool registers an agent-callable tool on the default module. Input and output JSON Schemas are derived from the In and Out type parameters via reflection; struct fields use their `json:"..."` tags. The handler receives parsed typed args and returns a typed result.
Name must satisfy registry.ValidateName (no path separators, whitespace, or null bytes). First-wins: a duplicate registration is a no-op (including its options).
The tool is served at POST /__mirrorstack/mcp/tools/call under Internal scope. The platform aggregates tools from all installed modules into a single agent-facing MCP server.
type GreetArgs struct{ Name string `json:"name"` }
type GreetResult struct{ Message string `json:"message"` }
ms.MCPTool("greet", "Say hi to someone",
func(ctx context.Context, a GreetArgs) (GreetResult, error) {
return GreetResult{Message: "hi " + a.Name}, nil
})
Generics give compile-time type safety on the handler signature; schemas are derived at registration via reflection and enforced against incoming JSON at call time (NOT statically against the wire format). Enforcement covers both constraints the derived schema states: a missing `required` argument and an unknown key (additionalProperties:false) are each rejected as ErrInvalidArgs rather than silently becoming a zero value.
DOCUMENT YOUR FIELDS. The model picks arguments from the input schema alone, so an undescribed field is one it has to guess at. Use the jsonschema struct tag — it needs no SDK API and shows up in tools/list immediately:
type GreetArgs struct {
Name string `json:"name" jsonschema:"description=Who to greet."`
}
Registration logs a warning naming every input field that has no description.
Optional MCPToolOptions scope and annotate the tool: ToolPermission gates it, and ToolReadOnly / ToolDestructive / ToolIdempotent / ToolTitle become the MCP annotations object. Declare ToolDestructive on anything a person would want to be asked about first — the platform agent renders it into the description the model reads.
Panics before Init or on schema derivation failure.
func Meter ¶
func Meter(name string, opts ...meter.MetricOption)
Meter declares a usage metric on the default module (side effect, no return). Panics before Init.
func ModuleDB ¶
ModuleDB returns a connection scoped to the module's shared schema on the default module.
func ModuleTx ¶
ModuleTx runs fn inside a transaction scoped to the module's shared schema on the default module.
func NewContributionSlot ¶
func NewContributionSlot[T any](key string) contributions.Slot
NewContributionSlot builds a Slot whose validator first unmarshals into T with DisallowUnknownFields, then validates against the same JSON Schema published in the manifest. Strict decoding rejects undeclared fields and trailing values; schema validation also enforces required keys that Go's decoder would otherwise silently replace with zero values. Schema tag is the Go type name, surfaced on the manifest.
func Notify ¶
func Notify(ctx context.Context, n Notification) error
Notify sends a notification on the default module created by Init(). Panics before Init — matches Call/CallGet/CallPost/Emit.
func OnEvent ¶
func OnEvent(name string, handler http.HandlerFunc, opts ...OnEventOption)
OnEvent registers an event handler on the default Module created by Init(). Panics before Init — matches Platform/Public/Internal.
func OnTask ¶
func OnTask(name string, handler TaskHandler, opts ...TaskOption)
OnTask registers a task handler on the default Module created by Init(). Panics before Init — matches Platform/Public/Internal.
func Provide ¶
Provide is the top-level entry point modules call from main.go to declare an extension slot others contribute to:
ms.Provide[ProviderContribution]("providers")
Builds the type-aware Slot and stores it on the default module. Panics on duplicate key or before ms.Init — matches the existing RegisterUI / RequirePermission startup-error conventions.
func RecordObservation ¶
func RecordObservation(ctx context.Context, name string, value float64, observation meter.Observation) error
RecordObservation emits a v2 usage observation on the default module. Panics before Init, matching Record and RecordWithID.
func RecordWithID ¶
RecordWithID emits a usage event using a caller-persisted UUID deduplication key. Panics before Init, matching Record.
func RegisterMessages ¶
RegisterMessages loads the module's i18n catalogs (one <locale>.json per file under dir) into the SDK's message registry. See i18n.RegisterMessages.
func RegisterPermission ¶
func RegisterPermission(name string, opts PermissionOpts)
RegisterPermission declares a permission on the default Module created by Init(). Calling this before Init() panics — match Platform/Public/Internal.
func RegisterUI ¶
func RegisterUI(ui ModuleUI)
RegisterUI declares the default module's UI surface. Panics if Init has not been called. See Module.RegisterUI.
func RequirePermission ¶
RequirePermission is the convenience wrapper that dispatches to the default Module created by Init(). Calling this before Init() panics — match the behavior of Platform/Public/Internal.
ms.Init(...)
ms.RegisterPermission("media.view", ms.PermissionOpts{DefaultRole: roles.Viewer()})
ms.Platform(func(r chi.Router) {
r.With(ms.RequirePermission("media.view")).Get(...)
})
func RequireStorage ¶
func RequireStorage()
RequireStorage declares storage on the default module created by Init.
func Resolve ¶
Resolve looks up a previously-registered client of type T by module ID. Returns the zero value of T and false when no client is registered for id (either because the dependency module isn't installed or no module has exported a T-typed client).
v1 note: cross-module client wiring is not yet designed. This stub always returns (zero, false). Real resolution lands with the catalog's install machinery.
func RunTaskWithIdempotencyKey ¶
func RunTaskWithIdempotencyKey(ctx context.Context, name string, payload json.RawMessage, idempotencyKey string) (string, error)
RunTaskWithIdempotencyKey dispatches a recoverable logical task on the default Module. Persist and reuse the UUID across ambiguous enqueue failures.
func StoredContributions ¶
func StoredContributions(ctx context.Context, slot string) ([]contributions.Contribution, error)
StoredContributions reads stored contributions for a slot on the default module. Panics if Init has not been called. See Module.StoredContributions.
Types ¶
type CallOptions ¶
type CallOptions struct {
// MaxResponseBytes bounds a successful response before JSON decoding. Zero
// keeps the existing unbounded behavior. Negative values are invalid.
MaxResponseBytes int64
// StrictJSON rejects response object fields not declared by out and rejects
// a second/trailing JSON value.
StrictJSON bool
}
CallOptions opts one inter-module response into stricter decoding. The zero value preserves Call's existing forward-compatible, unbounded response behavior.
type Compute ¶
type Compute struct {
// contains filtered or unexported fields
}
Compute is a VALIDATED execution-class descriptor. Its fields are unexported so the only way to obtain one is through Standard, Heavy or GPU — which is what makes "if you hold a Compute, it is legal" true, and lets WithCompute skip re-validating at registration.
func GPU ¶
GPU selects ECS on an allowlisted EC2 GPU instance — NVENC transcode and model inference.
The INSTANCE is the request: g5g.xlarge gives the task 4 vCPU / 7168 MiB / 1 GPU (with 1 GiB of host memory reserved for the OS and ECS agent), so accepting VCPU or MemoryMB alongside it would invite a contradiction the platform would have to silently resolve. Pick the instance; its allocatable dimensions are resolved into the descriptor for the manifest.
ms.WithCompute(ms.GPU(ms.Res{Instance: "g5g.xlarge"}))
func Heavy ¶
Heavy selects AWS Fargate — background work that does not fit Lambda's 15-minute ceiling or modest vCPU allocation (transcode, batch inference, bulk indexing).
VCPU and MemoryMB are both REQUIRED and must form a legal pair in fargateSizes. There is deliberately no default size: guessing one would spend the app owner's money on a number nobody declared or approved.
ms.WithCompute(ms.Heavy(ms.Res{VCPU: 4, MemoryMB: 8192}))
func Standard ¶
Standard selects AWS Lambda — the default execution class, and the one every task registered before WithCompute existed already runs on.
MemoryMB is the ONLY dial: Lambda derives vCPU from memory on a fixed curve, so a VCPU request here cannot be honoured and is a panic rather than a silent no-op. Zero MemoryMB means "platform default" and is legal, so Standard(Res{}) states today's behaviour explicitly.
Panics on programmer error, matching ExposeTable and OnTask: an illegal declaration is caught at module init, not at deploy.
ms.WithCompute(ms.Standard(ms.Res{MemoryMB: 512}))
type Config ¶
type Config struct {
ID string // Immutable module identifier (required). Minted once by the
// platform at first publish; never changes. Anchors the mod_<id> schema
// and the catalog primary key.
Slug string // Human-readable handle owned by the catalog. Mutable via
// catalog UI; the storage prefix at install time is <username>_<slug>_.
// Optional in dev; the publish pipeline is where slugs become required.
Name string // Default display name (platform can override)
Icon string // Default Material icon name (platform can override)
// Description is a short, plain-language summary shown in the catalog/agent discovery. Optional.
Description string
// DescriptionLabel is the optional per-locale form of Description, built from
// ms.Text (a literal) or ms.T (an i18n catalog key). When set it is resolved
// against the module's i18n catalogs at manifest build and folded into the
// manifest as descriptionLabels (locale → text) ALONGSIDE Description, so the
// platform can show a localized summary. Description stays the default /
// fallback; a zero Label is omitted entirely. Mirrors PermissionOpts.Description
// / meter.MetricLabel. Resolution is lazy (manifest build), so RegisterMessages
// may run before or after Init — only both-before-serve matters.
DescriptionLabel Label
// Tags are module-level category badges (e.g. "Auth", "Payments") shown in
// the platform's module catalog / settings. Surfaced via manifest defaults.
Tags []string
// SQL is an optional filesystem containing the module's sql/ directory
// (typically an embed.FS from `//go:embed sql/*`). The manifest endpoint
// reads it to determine the latest migration version, and the lifecycle
// routes (install/upgrade/downgrade) read it to apply migrations.
SQL fs.FS
// Readme is an optional filesystem holding the module's long-form README
// files (typically an embed.FS from `//go:embed README*.md`). README.md is
// the default; README.<tag>.md adds that locale.
//
// 🔴 THIS EXISTS SO A DEV-MOUNTED MODULE SHOWS ITS CURRENT README. The
// platform records a README map on a published VERSION, so a module running
// on a dev tunnel shows whatever its last release recorded — text that can be
// months old, and in the wrong language if the localized README was added
// since. Shipping it in the manifest lets the console read the running
// module, exactly as it already does for the description.
Readme fs.FS
// Versions optionally maps semver release tags to per-scope migration
// numbers, e.g.:
//
// {
// "v0.1.0": {App: "0008", Module: "0002"},
// "v0.2.0": {App: "0012"}, // module track unchanged
// }
//
// Exposed to the platform via the manifest endpoint so the platform can
// translate its internal semver-based deploy state into the migration
// numbers the lifecycle handlers accept. The SDK itself never reads this
// map at lifecycle time — /lifecycle/{upgrade,downgrade} take migration
// numbers only.
Versions map[string]system.MigrationVersions
// WebDir is the on-disk path to the module's React bundle output
// (typically "web/dist"). When set, the SDK serves it publicly under
// /__mirrorstack/web/* with permissive CORS so the platform's catch-all
// route can dynamically import the named exports declared in
// RegisterUI.DefaultPages. Optional — when empty, the /web route 404s.
WebDir string
// Client declares an optional custom-app client project. Dir is relative to
// the module root; OutputDir is relative to Dir. The CLI builds and publishes
// this output during `mirrorstack dev --tunnel`. Package identity, versions,
// registry details, and the build command are controlled by MirrorStack.
Client *system.ClientSpec
}
Config holds the module identity. Passed to Init() or New().
type DeliveryTicket ¶
type DeliveryTicket struct {
// Prefix is the platform-composed ABSOLUTE object-key prefix this ticket
// authorises: apps/<app>/<module>/_gated/…/. It is echoed by the platform so
// the module can assert what it actually got instead of reconstructing it.
Prefix string
// URLPrefix is the CDN origin joined to Prefix. The module never
// concatenates the CDN base itself — that string is platform configuration
// and the module must not hold a second opinion about it.
URLPrefix string
// ExpiresAt is when the credential dies. After it, the edge returns 403 and
// the viewer needs a re-mint (which is what re-fetching the playlist does).
ExpiresAt time.Time
// ExpiresIn is the CLAMPED lifetime the platform granted. It can be shorter
// than the ttl proposed — the proposal is a proposal — so a caller that
// wants to re-mint on schedule must read this rather than assume its own.
ExpiresIn time.Duration
// contains filtered or unexported fields
}
DeliveryTicket is ONE minted delivery credential together with the CDN origin it is valid against. One ticket covers every object under Prefix, so a caller with many keys — a transcode job is hundreds of segments across four renditions — mints ONCE and calls URL for each. Minting per segment would spend one platform round trip per playlist line.
The credential itself is unexported on purpose: the module's contract is "you get a URL", not "you get a token you may attach yourself". There is no accessor, so there is no call site that can build an almost-right URL.
func Delivery ¶
Delivery mints ONE delivery credential covering every object under prefix.
prefix is relative to this module's own storage prefix — exactly the key space ms.Storage operates in — and must start with GatedKeyPrefix and end with "/". Scope it to the narrowest thing the entitlement decision actually covered: for video that is one transcode job, which is one video across all its renditions. Do not scope it at "_gated/": that is "every private object this module owns", which is a bucket token wearing a smaller hat.
ttl is a PROPOSAL. The platform clamps it into its own bounds and reports the result as ExpiresIn; a zero ttl takes the platform default. Propose no more than the remaining lifetime of the entitlement the caller is acting on — a delivery credential outliving the entitlement that produced it is strictly worse than the presigned URL it replaces.
Not module-bound (no *Module receiver), and deliberately so: the platform derives the calling module from the CREDENTIAL, never from anything sent. A receiver would falsely suggest a module id travels in the request. Same shape and same reason as AppID.
🔴 Fail closed. Every error path returns the zero ticket, which cannot produce a URL. There is no degraded mode.
func (DeliveryTicket) URL ¶
func (t DeliveryTicket) URL(relativeKey string) (string, error)
URL returns the delivery URL for one object, addressed RELATIVE to the ticket's prefix. relativeKey is the tail: for a ticket on "_gated/videos/v1/transcodes/j1/720p/" that is "seg_000042.ts".
It never returns a URL without the credential attached — a ticket that somehow reached a caller unarmed is an error, not a public URL.
type Dependency ¶
type Dependency struct {
// contains filtered or unexported fields
}
Dependency is the read handle DependencyDB returns: the resolved (app, consumer, producer) triple every query built from it targets. Construction never fails — a bad ref or missing app scope is carried forward and surfaced by Rows/Result, keeping call sites chainable.
func DependencyDB ¶
func DependencyDB(ctx context.Context, producerRef string) *Dependency
DependencyDB returns a read handle on the default module. Panics before Init — matching Platform/Public/Internal. See Module.DependencyDB.
func (*Dependency) Select ¶
func (d *Dependency) Select(table string) *DependencyQuery
Select starts a structured read of one exposed table. name is the LOGICAL table name the producer declared via ms.ExposeTable (e.g. "users") — lowercase snake_case, never the physical prefixed relation name.
type DependencyQuery ¶
type DependencyQuery struct {
// contains filtered or unexported fields
}
DependencyQuery is the structured read builder. All builder methods validate eagerly and latch the FIRST error; Rows/Result surface it without touching the network. The builder is single-use and not safe for concurrent mutation (build, then execute).
func (*DependencyQuery) Columns ¶
func (q *DependencyQuery) Columns(cols ...string) *DependencyQuery
Columns restricts the projection to the named columns. Accumulates across calls; at most 64 names. An empty projection is resolved by the read-exposed PROXY only, which expands it to all visible columns; the deployed plane and a co-located dev read both require at least one column — the blessed dynamic-SELECT builder never emits SELECT *.
func (*DependencyQuery) Limit ¶
func (q *DependencyQuery) Limit(n int) *DependencyQuery
Limit caps the row count. The platform defaults to 200 when unset (or <= 0) and hard-caps at 2000; a read cut at the limit reports Truncated=true via Result.
func (*DependencyQuery) Result ¶
func (q *DependencyQuery) Result(ctx context.Context) (*DependencyResult, error)
Result executes the read and returns rows plus the Truncated flag. Failure modes are typed and fail-closed (never silently empty) — see the Err* sentinels.
func (*DependencyQuery) Rows ¶
Rows executes the read and returns the decoded rows — one map per row, column name to value. Numeric values decode as json.Number (not float64) so int64 join keys keep full fidelity for the fetch-then-join in app code. Rows is never nil on success. When the read hits the limit the extra rows are simply absent here — use Result if you need the Truncated flag.
func (*DependencyQuery) Where ¶
func (q *DependencyQuery) Where(col string, value any) *DependencyQuery
Where adds an equality predicate: col = value. value must be a JSON scalar (string, bool, or a numeric type) — the proxy's filter language is exactly scalar equality and scalar IN, nothing else (no NULL matching, no ranges, no nesting). Predicates across columns are ANDed; at most 16 filter columns, one predicate per column.
func (*DependencyQuery) WhereIn ¶
func (q *DependencyQuery) WhereIn(col string, values ...any) *DependencyQuery
WhereIn adds an IN predicate: col IN (values...). 1..200 JSON scalar values; an empty list is rejected (an always-false predicate is almost certainly a bug — fail fast rather than return silently empty rows).
type DependencyResult ¶
DependencyResult is one executed read: the decoded rows plus whether the read was cut at the limit (more rows exist). Rows is never nil.
type DynamicSelect ¶
type DynamicSelect struct {
Schema string
Table string // physical relation name from the manifest, e.g. m<hex>_<table>
Columns []string
Filters []SelectFilter
Limit int // <=0 → defaultDynamicSelectLimit; clamped to maxDynamicSelectLimit
}
DynamicSelect describes the one dynamic statement shape composed here: a projection of whitelisted columns from one physical relation, ANDed equality/IN filters, and a row limit. The caller NEVER supplies SQL.
type Label ¶
directly.
type MCPToolOption ¶
type MCPToolOption func(*mcpToolConfig)
MCPToolOption configures an MCPTool declaration. The variadic seam carries future per-tool knobs (e.g. risk tier) without breaking existing callers.
func ToolDestructive ¶
func ToolDestructive() MCPToolOption
ToolDestructive declares that the tool changes or removes data irreversibly. The platform agent renders this into the tool description the model reads, so it is a real warning and not decoration.
Declare it on anything a person would want to be asked about first. An absent hint is not a promise of safety, but it is also not a warning — so a destructive tool that stays silent gets treated like any other.
func ToolDigest ¶
func ToolDigest[Out any](fn func(Out) any) MCPToolOption
ToolDigest declares how THIS tool's result compresses, for replay on later turns.
🔴 WHY A MODULE HAS TO SAY THIS AND THE PLATFORM CANNOT GUESS. Tool results are replayed to the model so a later turn can still cite them, and a large one has to shrink somehow. Without a digest the only option is a blind byte truncation — which keeps the FIRST n bytes, and the first n bytes of a fifty-row table is rows one to four. The module knows that "50 users, first five: …, filter by role to narrow" answers more later questions in fewer tokens than rows one to four do. Nothing outside the module can know that.
The digest runs only when the full result is large enough to be worth shrinking; a small result replays verbatim. It must never fail the call: a digest that errors is dropped and the caller falls back to truncation, so a bad digest costs fidelity and never the tool.
ms.MCPTool("users-list", "List users", listUsers,
ms.ToolDigest(func(r UsersResult) any {
return map[string]any{"total": r.Total, "sample": r.Users[:min(5, len(r.Users))]}
}),
)
func ToolIdempotent ¶
func ToolIdempotent() MCPToolOption
ToolIdempotent declares that calling the tool twice with the same arguments has the same effect as calling it once — which is what makes a retry safe.
func ToolPermission ¶
func ToolPermission(name string) MCPToolOption
ToolPermission gates the tool on a module permission, looked up by SHORT name and slug-qualified exactly like RegisterPermission. The platform lists and invokes the tool only for callers whose effective permissions include it.
Safe-by-default: if the name was never RegisterPermission'd, it is registered lazily as ADMIN-ONLY (a dev warning is logged) so a typo locks the tool down rather than opening it — same rule as RequirePermission.
func ToolReadOnly ¶
func ToolReadOnly() MCPToolOption
ToolReadOnly declares that the tool does not modify anything.
🔴 DEFAULT UNSET IS NOT DEFAULT READ-ONLY. A tool that says nothing is UNKNOWN, and every reader must treat unknown as "may write". Defaulting to readOnly:true would silently vouch for every tool ever written, including the ones that delete things — which is exactly backwards for a hint whose only job is to let a caller relax.
func ToolTitle ¶
func ToolTitle(title string) MCPToolOption
ToolTitle sets a short human-readable display name for the tool, shown by clients that render a tool list for a person. The wire NAME stays the identifier; this is only for display.
type MemberAssertion ¶
type MemberAssertion struct {
// Token is the opaque platform-signed assertion. Treat it as a bearer
// credential: anyone holding it can act as this member until it expires.
Token string
// ExpiresAt is when the assertion dies. After it, dispatch rejects the
// request outright rather than downgrading it to anonymous.
ExpiresAt time.Time
// ExpiresIn is the CLAMPED lifetime the platform granted, which can be
// shorter than the ttl proposed. A holder that renews on a schedule must
// read THIS, not its own proposal — renewing off the proposal is how a
// clamped credential expires mid-session.
ExpiresIn time.Duration
}
MemberAssertion is ONE minted, platform-signed statement that a named member is acting. Present Token to a module's /public/* route as `Authorization: Bearer <token>` and dispatch verifies it and stamps the user id the same way it already does on /platform/*.
UNLIKE DeliveryTicket, THE CREDENTIAL IS EXPORTED, and that is a real cost paid deliberately. A delivery ticket hides its token because the module's contract is "you get a URL"; an assertion exists precisely so the auth module can hand the string onward to the app's BFF, so there is nothing to hide it behind. DeliveryTicket therefore fails closed through TWO layers (response screening plus the armed check inside URL); this type has only the first. Wrapping Token in an accessor would be the same single layer with extra steps. So: the screening in newMemberAssertion is the whole guarantee, and it is pinned by its own test rather than only through the mint path.
The zero value carries an empty Token and is therefore unusable — which is what every error path returns.
func MintMemberAssertion ¶
func MintMemberAssertion(ctx context.Context, userID string, ttl time.Duration) (MemberAssertion, error)
MintMemberAssertion exchanges a decision this module already made — "this request belongs to member userID" — for a platform-signed assertion other modules can trust.
ONLY THE MODULE HOSTING THE auth-provider SLOT MAY CALL THIS. The platform checks that at mint time against a fact recorded when the app was installed; any other module gets an error, whatever it sends. That check is the whole reason this endpoint is safe to expose at all: unlike a delivery token, which can only ever address the caller's own storage prefix, an identity assertion names SOMEBODY ELSE and has no naturally bounded scope.
userID must be the member's UUID as the auth module knows it.
ttl is a PROPOSAL. The platform clamps it (5 minutes today) and reports the result as ExpiresIn; a zero ttl takes the platform default. Propose no more than the remaining life of the session the assertion is derived from.
Not module-bound (no *Module receiver), and deliberately so — same shape and same reason as Delivery and AppID: the platform derives the calling module from the CREDENTIAL, never from anything sent, and a receiver would falsely suggest a module id travels in the request.
PACKAGE-LEVEL FUNCTION, NOT AN INTERFACE METHOD, and this is the deliberate answer to the PrefixDeleter-vs-Storer question. Storer's rule is that a method goes on the interface only when it is fundamental to what every holder of that interface does, and stays BESIDE it otherwise, because adding one is source-breaking for every implementation and test double outside the package. Minting an assertion is fundamental to nothing a module already holds: it is not a storage operation, not a cache operation, and it has no second implementation to abstract over — there is exactly one platform. Putting it on an interface would invent a seam whose only purpose is to let something OTHER than the platform mint identity, which is the one thing this design exists to prevent. It is a free function for the same reason Delivery is.
🔴 Fail closed. Every error path returns the zero MemberAssertion, whose Token is empty. There is no degraded return, because the degraded return here is the dangerous one: an empty or malformed token sent on a request makes every downstream module see an ANONYMOUS caller while the auth module believes it signed someone in — a member who is quietly served the signed-out answer, and an audit row that names no actor for a human action.
type Module ¶
type Module struct {
// contains filtered or unexported fields
}
Module is the core SDK instance.
internalAuth is captured at New() time so OnEvent/Cron registrations can reuse a single middleware closure. auth.InternalAuth() reads MS_INTERNAL_SECRET once at construction; constructing it per registration would re-read the env var and re-allocate the closure on every call.
func DefaultModule ¶
func DefaultModule() *Module
DefaultModule returns the default module for advanced use cases.
func (*Module) AbsorbInfra ¶
func (m *Module) AbsorbInfra()
AbsorbInfra declares that this module passes EVERY platform infrastructure metric through to the app owner at a price of 0 — compute, egress, storage, AI tokens, and whatever the platform meters next. Use it when the module bills its customer through its OWN meters and adds no infrastructure passthrough on top.
🔴 THE MODULE NAMES NOTHING. This is one manifest flag; the platform expands it against its own metric catalog at publish, so a metric introduced after this module shipped is absorbed with no republish. The alternative — a Meter("infra.X", Price(0)) line per metric — is a copy of a catalog the module does not own, and it has already gone stale once: infra.compute.ms was re-chartered to infra.compute.walltime.ms and then deleted, so every module still naming it declared nothing at all.
An explicit Meter("infra.X", Price(n)) is applied AFTER the absorb and WINS:
ms.AbsorbInfra() // baseline: all infra at 0
ms.Meter("infra.egress.api.bytes", ms.Price(120_000)) // …except egress
Absorbing changes what the APP OWNER is billed for that metric. It does not make the underlying cost disappear.
func (*Module) Cache ¶
Cache returns a scoped cache client. Keys are auto-prefixed with {appID}:{moduleID}:.
c, release, err := mod.Cache(r.Context())
if err != nil { ... }
defer release()
c.Set(ctx, "views:123", "42", 5*time.Minute)
val, err := c.Get(ctx, "views:123")
func (*Module) Call ¶
func (m *Module) Call(ctx context.Context, targetModuleID, method, path string, body, out any) error
Call makes one server-mediated module-to-module hop through the platform dispatch. It marshals body to JSON (omitted for a nil body), POST/GET/etc to the resolved dispatch URL scoped to the current app, and decodes the JSON response into out (skipped when out is nil).
The app id is read from the request context (auth.Get) — the same identity the SDK injects for handlers — and sent as X-MS-App-ID so the dispatch can resolve the install. X-MS-Service-Secret carries this CALLING module's live session/deploy credential, allowing dispatch to authenticate which installed module initiated the internal hop. The caller never holds the CALLEE's credentials: dispatch injects the target module's per-session token and identity before forwarding.
path must include its leading slash and any raw query string, e.g. "/internal/exchange" or "/platform/users?limit=10". A verified actor delegation is forwarded only to a canonical /platform route. Public and Internal calls remain actorless service calls even when they originate in a Platform handler.
DEV/DISPATCH TRANSPORT — see resolveCallURL for the prod (#146) seam.
func (*Module) CallDependency ¶
func (m *Module) CallDependency(ctx context.Context, producerRef, method, path string, body, out any) error
CallDependency addresses a declared dependency by the same stable ref used in DependsOn. The platform authenticates this module, checks the live dependency declaration, resolves the producer within the current app, and forwards an actorless request to the producer's Internal surface.
func (*Module) CallDependencyPost ¶
func (m *Module) CallDependencyPost(ctx context.Context, producerRef, path string, body, out any) error
CallDependencyPost is CallDependency specialized to POST with a JSON body.
func (*Module) CallGet ¶
CallGet is Call specialized to GET (no request body). path carries any raw query string, e.g. "/internal/users?limit=10".
func (*Module) CallWithOptions ¶
func (m *Module) CallWithOptions(ctx context.Context, targetModuleID, method, path string, body, out any, options CallOptions) error
CallWithOptions is Call with opt-in response bounds and strict JSON decoding. A negative MaxResponseBytes is rejected before any network I/O. When out is nil, a successful response body is not read or decoded.
func (*Module) CancelTask ¶
CancelTask requests monotonic cancellation of a local or managed job. Cancellation is idempotent for queued, running, already-cancelled, and terminal jobs.
func (*Module) ContributesTo ¶
ContributesTo declares that this module pushes payload INTO another module's contribution slot — the contributor side of Provide. It is a ZERO-RUNTIME declaration that mirrors DependsOn: the payload becomes manifest metadata (contributesTo), and the platform catalog — or the CLI dev runner in local dev — performs the actual registration into the host after app-owner approval. The module's binary never calls the host directly.
hostSpec takes the same id or id@constraint form as DependsOn, and for the same reason: a contribution couples you to the host's declared SLOT surface exactly as a dependency couples you to its API, and a host may reshape or remove a slot on any version bump — the pinned range is what lets the catalog refuse a contribution against a host version that no longer declares the slot. Pair it with ms.DependsOn(hostSpec) so the platform installs the host first and can validate the slot against the host's provides.
payload must be a JSON-serializable value matching the host slot's declared type (e.g. oauth-core's AuthProvider). Re-declaring the same (host, slot) replaces the payload and the constraint — last call wins.
Panics on an empty host/slot, a host id or SemVer constraint that does not parse, or an unmarshalable payload — all programmer error, caught at startup rather than shipping a manifest entry nobody can resolve.
ms.ContributesTo("oauth-core@^1.2", "auth-provider", AuthProvider{
Slug: "google", Name: "Google", Icon: "login",
AuthorizeURL: "/authorize-url", CallbackURL: "/callback",
})
func (*Module) Cron ¶
func (m *Module) Cron(name, schedule string, handler http.HandlerFunc)
Cron registers a handler to run on a cron schedule. The handler is mounted on this module's Internal scope at /__mirrorstack/crons/{name}. The schedule is recorded in the manifest so the platform's scheduler knows what URL to POST when the cron fires.
The schedule string is passed through to the platform unchanged — validation happens platform-side so module developers see one error format regardless of the scheduler implementation.
Names must not contain path separators (/, \), whitespace, dot-segments (..), or null bytes. Do not rely on r.RemoteAddr to identify the scheduler; in Lambda + API Gateway it is always the AGW IP. Call from startup code, not a request handler.
Panics on empty schedule, an invalid name, or duplicate name registration.
mod.Cron("cleanup-temp", "0 3 * * *", cleanupHandler)
func (*Module) DB ¶
DB returns a scoped database connection.
Production: uses per-app credentials injected by the platform via Lambda payload. Dev: uses DATABASE_URL env var with localhost fallback. Dev connections are wrapped with the cross-module fail-fast guard (db_guard.go): a raw query against another module's tables errors immediately instead of silently succeeding in the shared dev database — use ms.DependencyDB for that.
conn, release, err := mod.DB(r.Context())
if err != nil { ... }
defer release()
conn.QueryRow(ctx, "SELECT ...").Scan(&v)
func (*Module) DependencyDB ¶
func (m *Module) DependencyDB(ctx context.Context, producerRef string) *Dependency
DependencyDB returns a read handle on producerRef's exposed tables within the current app. producerRef accepts the same module ref forms as ms.DependsOn — "@owner/slug", bare "slug", the m<hex> module ID, or a dashed UUID; a trailing @<version-constraint> is tolerated and ignored (reads always target the version the producer actually runs, which the platform resolves). The app scope is read from ctx via the SDK's auth identity — callers don't pass it and cannot forge it.
The returned handle only builds STRUCTURED reads (Select/Columns/Where/ WhereIn/Limit). It is not a *pgx pool and never will be — see the package comment for why raw cross-plane SQL cannot exist.
func (*Module) DependsOn ¶
DependsOn declares a REQUIRED dependency on another module. Install fails if no published version of the dep matches the constraint.
Spec syntax: "<id>" (any version) or "<id>@<constraint>" where constraint is an npm-style SemVer range. The id may be bare (`oauth-core`) or owner-prefixed (`@anna/oauth`). Examples:
ms.DependsOn("@anna/oauth") // any version
ms.DependsOn("@anna/oauth@^1.2.0") // >=1.2.0, <2.0.0
ms.DependsOn("@anna/oauth@1.2.3") // exact
ms.DependsOn("oauth-core@>=1.2 <2") // bare id, range
Optional configure callbacks declare what the consumer wants from the dep — relation reads and event subscriptions:
ms.DependsOn("@anna/oauth@^0.4.0", func(n *ms.Need) {
n.Table("oauth_users")
n.Event("user_signed_in")
})
The catalog validates each Table name against the dep's `mod_<id>` schema and each Event name against the dep's manifest at install time; after app-owner approval, the platform issues one GRANT SELECT per Table and wires event routing per Event.
Panics on an invalid SemVer constraint — programmer error, not runtime input.
For OPTIONAL deps (the consumer's handler only runs when the dep is installed — typically an event subscription), use ms.OptionalDependOn inside the handler-registration call.
func (*Module) DrainAudit ¶
DrainAudit forwards at most one bounded batch from the current app's audit outbox. Rows are leased with a rotating fence, so callers may safely run this concurrently from post-commit hooks, cron handlers, and maintenance jobs.
Delivery is at-least-once. API Platform de-duplicates EventID within the invocation-proven app and module stream.
func (*Module) Emit ¶
Emit publishes an event to every LIVE module that subscribes to name within the current app. It wraps payload in the event envelope (a fresh UUID id, the emitting module's Config.ID as sourceModuleID, and an RFC3339 UTC sentAt), then POSTs the envelope to the platform dispatch event bus scoped to the current app. The dispatch fans the SAME envelope out to each subscriber session — the emitter never addresses subscribers directly.
The app id is read from the request context (auth.Get) — the same identity the SDK injects for handlers — and is encoded in the dispatch URL. An empty app id is an error (no panic): Emit needs an app scope to know which installs can receive the event.
Delivery is AT-LEAST-ONCE and best-effort per subscriber (the dispatch logs per-subscriber failures and never fails the whole call); subscriber handlers must be idempotent. A non-2xx response from the dispatch itself is returned as an error with the response body truncated to ~2 KB.
DEV/DISPATCH TRANSPORT — see resolveEventBusURL for the prod (#146) seam.
func (*Module) Emits ¶
Emits declares that this module emits an event of the given name. The declaration appears in the manifest's events.emits list so the platform knows the event belongs to this module's vocabulary and other modules can subscribe to it. Runtime emission is a separate API (TBD); this method is purely a declaration.
Panics on an invalid name (see OnEvent for the rules) or duplicate declaration. Call from startup code.
func (*Module) ExposeTable ¶
ExposeTable marks one of this module's per-app tables as eligible to be read (SELECT) by a depending module installed in the same app. Declare the bare table name; the platform resolves it to the physical form — app_<id>."<prefix><table>" in each app's tenant schema — when granting. It is a pure DECLARATION — no runtime, no return value — recorded in the manifest under `exposes.tables` so the platform catalog can issue GRANT SELECT after the app owner approves a dependency. Cross-app module state (the mod_<id> schema) is NOT shareable this way.
v1 is TABLES ONLY, read-only. The producer marks a table READABLE; it does NOT name WHO reads it. The app owner — the trust root — decides which installed modules may read by approving their declared dependency. There is intentionally no per-consumer allowlist here: a marketplace's consumers are third parties, so a publisher-controlled reader list is the wrong trust model.
Panics on an empty or otherwise invalid table identifier (lowercase, leading letter, [a-z0-9_], <=63 chars). Call from startup code, not a request handler.
ms.ExposeTable("orders")
func (*Module) Internal ¶
Internal registers routes with internal auth scope (platform-to-module only). Auto-mounted under /internal/ — write `r.Post("/sessions", ...)` and the route lands at /internal/sessions. Validates X-MS-Internal-Secret via constant-time comparison. The middleware is cached on the Module at New() so OnEvent / Cron registrations reuse a single closure instead of constructing one per call.
func (*Module) Meter ¶
func (m *Module) Meter(name string, opts ...meter.MetricOption)
Meter DECLARES a usage metric. Call it once, up front (startup code), per metric — exactly like Emits / RegisterPermission: it registers the metric as a SIDE EFFECT and returns NOTHING. The declaration (kind + unit + price) is recorded in the manifest so the platform's metric catalog is authoritative before any event arrives, AND in the meter client's by-name registry so Record can resolve the metric.
The kind is an OPTION: meter.Counter (additive; the platform SUMs) or meter.Gauge (absolute level; the platform takes MAX or a time-weighted integral, never a SUM). meter.Unit / meter.Price set the unit and the per-unit customer price (both optional). meter.BySubject is valid only with Gauge and declares SUM(MAX(value) per observation subject).
Emit at runtime with the package-level Record(ctx, name, value) — BY NAME, mirroring Emits/Emit. A custom metric MUST pass exactly one kind option; a reserved infra.*/platform.* metric may carry meter.Price ONLY (kind/unit/ aggregation are platform-owned). Panics on a duplicate name, an invalid name, conflicting kinds, a missing kind on a custom metric, BySubject without Gauge, or a kind/unit/aggregation on a reserved name. Like the other Module resource methods, it requires New() to have returned successfully — calling it on a zero Module panics on the nil meterClient (the package-level wrapper's mustDefault guards the before-Init case).
ms.Meter("orders.placed", ms.Counter, ms.Unit("order"), ms.Price(50_000))
ms.Meter("users.active", ms.Gauge, ms.BySubject, ms.Unit("user"))
ms.Meter("infra.compute.walltime.ms", ms.Price(0)) // reserved price-override
To zero EVERY platform infra metric at once — the usual want for a module that charges through its own meters — call AbsorbInfra instead of listing them. A metric name written here is a copy of the platform's catalog and goes stale the first time the platform renames one (infra.compute.ms already did).
func (*Module) ModuleDB ¶
ModuleDB returns a connection scoped to this module's shared schema (mod_<id>). Use it for cross-app state — outbox tables, dedup ledgers, cross-app audit logs, rate limiters, module-wide config — anything a module needs that is owned by the module rather than by a single app.
Production: uses the per-module credential injected by the platform via db.WithModuleCredential. Independent of the per-app credential read by Module.DB — a handler that needs both gets both, in the same context.
Dev: shares the dev pool from DATABASE_URL with Module.DB; the schema is the only difference. The dev Postgres must have a mod_<id> schema for the queries to succeed.
The module schema overlays any app schema set on the caller's context for this single call only — the caller's ctx is not mutated, so a subsequent Module.DB call sees the original app schema unchanged.
conn, release, err := mod.ModuleDB(r.Context())
if err != nil { ... }
defer release()
conn.Exec(ctx, "INSERT INTO outbox(...) VALUES(...)")
func (*Module) ModuleTx ¶
ModuleTx runs fn inside a transaction scoped to the module's shared schema (mod_<id>). Commits on success, rolls back on error or panic. Use this for the outbox-pattern: insert the work record AND the dedup row in the same transaction so the worker only ever sees consistent state.
err := mod.ModuleTx(r.Context(), func(q db.Querier) error {
queries := generated.New(q)
if err := queries.InsertOutbox(ctx, ...); err != nil { return err }
return queries.MarkProcessed(ctx, ...)
})
func (*Module) Notify ¶
func (m *Module) Notify(ctx context.Context, n Notification) error
Notify sends an in-app notification to the current app's members. It resolves the Notification's i18n Labels to per-locale maps, wraps them in the notification envelope (a fresh UUID id, the sending module's Config.ID as sourceModuleID, and an RFC3339 UTC sentAt), then POSTs the envelope to the platform dispatch notification ingress scoped to the current app. The dispatch re-derives the sender identity from the live session — the envelope's sourceModuleID is informational, never trusted.
The app id is read from the request context (auth.Get) — the same identity the SDK injects for handlers — and is encoded in the dispatch URL. An empty app id is an error (no panic): Notify needs an app scope to know whose feed receives the notification.
Title is REQUIRED — a Title that resolves to no message (unset, or an empty literal) is an error (no panic). Body is optional (an unset Body sends an empty map). An unset Audience defaults to NotifyAdmins; any value other than NotifyAdmins/NotifyAllMembers is an error. A non-2xx response from the dispatch is returned as an error with the response body truncated to ~2 KB — never with the credential.
Works on both planes: under `mirrorstack dev --tunnel` the credential is the CLI's per-session secret; on a deployed module it is the per-module value the deploy provisioner injects. Both arrive as MS_INTERNAL_SECRET and are sent verbatim (see moduleSessionSecret).
func (*Module) OnEvent ¶
func (m *Module) OnEvent(name string, handler http.HandlerFunc, opts ...OnEventOption)
OnEvent registers handler for events of the given name from another module. Mounts on this module's Internal scope at /__mirrorstack/events/{name} and records the subscription in the manifest's events.subscribes map.
Optional dependency declarations co-locate via the variadic opts:
ms.OnEvent("@anna/billing/payment", onPayment,
ms.OptionalDependOn("@anna/billing@^1", func(n *ms.Need) {
n.Table("invoices")
}))
Each OptionalDependOn-produced option appends an optional dep to the manifest. The dep is "scoped" to this handler in spirit — if the dep isn't installed, the event never fires and the handler never runs, so missing-dep failures are harmless.
The platform guarantees AT-LEAST-ONCE delivery — handlers must be idempotent or implement their own deduplication.
Names must not contain path separators (/, \), whitespace, dot-segments (..), or null bytes. Call from startup code (init / main), not from inside a request handler. Panics on duplicate registration.
func (*Module) OnTask ¶
func (m *Module) OnTask(name string, handler TaskHandler, opts ...TaskOption)
OnTask registers a background task handler. The handler appears in the manifest so the platform can admit managed task jobs. A dev HTTP endpoint is mounted at POST /__mirrorstack/tasks/{name} on the Internal scope for curl testing.
Names must not contain path separators (/, \), whitespace, dot-segments (..), or null bytes. Call from startup code (init / main), not from inside a request handler.
Panics on duplicate registration with the same name.
The execution class defaults to Standard — the module's own Lambda, with Lambda's 15-minute ceiling. Pass WithCompute to route the task to a heavier runner instead; the platform provisions it from the manifest at deploy.
mod.OnTask("thumbnail", thumbnailHandler, ms.WithTimeout(10*time.Minute))
mod.OnTask("transcode-video", transcodeHandler,
ms.WithCompute(ms.Heavy(ms.Res{VCPU: 4, MemoryMB: 8192})),
ms.WithTimeout(90*time.Minute),
ms.WithMaxRetries(2),
)
func (*Module) Platform ¶
Platform registers routes with platform auth scope. All routes declared inside fn are auto-mounted under /platform/ — write `r.Get("/users", ...)` and the route lands at /platform/users. Default role gate: admin only. Use Module.RequirePermission to open routes to member/viewer.
func (*Module) ProvideSlot ¶
func (m *Module) ProvideSlot(slot contributions.Slot)
built once at the call site and the Module just stores the resulting Slot.
func (*Module) Public ¶
Public registers routes with public auth scope (anyone, including anonymous). Auto-mounted under /public/ — write `r.Get("/me", ...)` and the route lands at /public/me.
func (*Module) Record ¶
Record emits a usage event for the metric declared (via Meter) under name — BY NAME, mirroring Emit. Resolves the declared metric from the meter client's registry; returns an error (never panics) if name was never declared, or if value is negative/non-finite. A billing failure must never fail the handler, so the error should be logged, not propagated.
func (*Module) RecordObservation ¶
func (m *Module) RecordObservation(ctx context.Context, name string, value float64, observation meter.Observation) error
RecordObservation emits a v2 usage observation. Its caller-persisted event ID, subject, metadata, and occurrence time survive outbox retries; platform Dispatch still re-derives authoritative app/module/owner attribution. The SDK validates structure and normalizes OccurredAt to UTC without comparing it with the module process clock. API and Billing authoritatively decide the accepted occurrence window, replay outcome, and billing period.
func (*Module) RecordWithID ¶
RecordWithID is Record with a caller-persisted UUID deduplication key. It is intended for transactional outboxes whose retries must remain idempotent.
func (*Module) RegisterPermission ¶
func (m *Module) RegisterPermission(name string, opts PermissionOpts)
RegisterPermission declares a module permission ONCE, recording its default role, custom roles, and resolved i18n Labels/Descriptions into this Module's registry so they appear in the manifest. Call it at startup (typically in a declare/ package) BEFORE the routes that gate on it. First-wins by name: re-declaring the same permission is a no-op.
Permission names are auto-namespaced with the module slug so two modules can each declare a "users.read" without colliding in the platform's catalog. Developers pass the SHORT name; the manifest stores "<slug>.<name>". If the caller already prefixed the name with the slug, the prefix is left alone.
import "github.com/mirrorstack-ai/app-module-sdk/roles"
ms.RegisterPermission("users.read", ms.PermissionOpts{
DefaultRole: roles.Viewer(),
Label: ms.T("permissions.users.read"),
})
func (*Module) RegisterUI ¶
RegisterUI declares the module's UI surface — agent-visible Components plus DefaultPages mounted under /apps/<app-slug>/<module-slug>. See the ModuleUI doc comment for the shape. Validates the input and panics on programmer errors (duplicate names, invalid route, unknown prop type). Last-write-wins; a second call replaces the prior manifest.
ms.RegisterUI(ms.ModuleUI{
Components: []ms.UIComponent{
{Name: "SettingsForm", Export: "SettingsForm", Props: []ms.UIProp{
{Key: "appId", Type: "text", Required: true},
}},
},
DefaultPages: []ms.UIPage{
{Route: "/", Title: "OAuth Settings", Export: "SettingsPage"},
},
})
func (*Module) RequirePermission ¶
RequirePermission returns chi middleware that gates a route on a permission declared via RegisterPermission, looked up BY NAME. It reads the registered permission's role set and enforces it (admin always passes). Call this at route registration time (alongside m.Platform/Public/Internal), NOT from inside a request handler.
import "github.com/mirrorstack-ai/app-module-sdk/roles"
// in declare/: ms.RegisterPermission("users.view", ms.PermissionOpts{DefaultRole: roles.Viewer()})
// in routes:
r.With(mod.RequirePermission("users.view")).Get("/items", listItems)
The name is slug-qualified the same way RegisterPermission qualifies it, so the short name used here matches the short name used there.
Safe-by-default: if the name was never RegisterPermission'd, it is registered lazily as ADMIN-ONLY (an isDev warning is logged) so a forgotten declaration locks the route down rather than opening it.
func (*Module) RequireStorage ¶
func (m *Module) RequireStorage()
RequireStorage opts this module into its per-app storage namespace. Call it once during startup before Start. The declaration is emitted in the manifest and is the platform's gate for vending temporary storage credentials.
func (*Module) RunTask ¶
RunTask creates a task job and returns its job ID. Deployed requests contain only the caller's raw JSON payload; execution identity, resources, compute and artifacts are resolved by the platform. CLI dev mode instead enqueues the registered handler asynchronously in a process-local executor.
taskID, err := mod.RunTask(r.Context(), "transcode-video", payload)
func (*Module) RunTaskWithIdempotencyKey ¶
func (m *Module) RunTaskWithIdempotencyKey(ctx context.Context, name string, payload json.RawMessage, idempotencyKey string) (string, error)
RunTaskWithIdempotencyKey creates or recovers one logical task job. Persist the UUID with the caller's logical work before enqueueing, then reuse it after ambiguous failures or process restarts. Both the local executor and managed platform return the original job for an exact retry and reject key reuse with a different task or payload.
func (*Module) Start ¶
Lambda wins if both env vars are set (they are mutually exclusive in production but this ordering is a safety net).
func (*Module) Storage ¶
Storage returns a client always scoped to apps/<app>/<module>/.
s, err := mod.Storage(r.Context())
if err != nil { ... }
url, err := s.PresignPut(ctx, "photo.jpg", 15*time.Minute)
cdnURL, err := s.URL("photo.jpg")
Production enforces that scope with the IAM session policy on the vended credential; the client prefix is ergonomics, not a boundary. Dev mints the same policy shape and MinIO enforces it.
func (*Module) StoredContributions ¶
func (m *Module) StoredContributions(ctx context.Context, slot string) ([]contributions.Contribution, error)
StoredContributions returns the contributions OTHER modules have registered into one of THIS module's slots (slot key as declared via Provide), newest first. It reads the SDK-managed <moduleID>_contributions store — the canonical registry the platform's install-time auto-register writes to. Each entry's OwnerModuleID is the contributing module's canonical UUID, which a host can use to address that module. ID remains populated as a compatibility alias. Use this on the HOST side to consume contributions (e.g. oauth-core listing its registered auth providers).
A host installed before the SDK provisioned this store on install has no table for the app yet; the first read creates it (see Storage.WithTable) and returns empty, instead of failing the host with a missing-relation error it has no way to act on.
func (*Module) TaskStatus ¶
TaskStatus reads process-local state in CLI dev mode. Deployed jobs are scoped to the caller's app and this module; a sibling module cannot inspect them by guessing an ID.
func (*Module) Tx ¶
Tx runs fn inside a transaction with schema isolation. Commits on success, rolls back on error. The app schema is read from the caller's context (set by the platform's Lambda invoke shim via db.WithSchema). Compare with Module.ModuleTx which explicitly overlays the mod_<id> schema.
err := mod.Tx(r.Context(), func(q db.Querier) error {
queries := generated.New(q)
item, err := queries.GetItem(ctx, id)
if err != nil { return err }
return queries.DeductBalance(ctx, params)
})
type ModuleUI ¶
Public type aliases for the UI manifest. The data shapes live in the registry package (alongside the storage methods); core re-exports them so module authors construct ms.ModuleUI{...} without importing internal/.
type Need ¶
type Need struct {
// contains filtered or unexported fields
}
Need is the configuration handle passed to ms.DependsOn / ms.OptionalDependOn callbacks. It collects what the consumer wants from a dependency module — relations to read and events to subscribe to. The handle is opaque: Table and Event are the only mutators, so a caller can't bypass them by constructing a Need{...} literal.
func (*Need) Event ¶
Event records an event name the consumer subscribes to from this dep. Used by the catalog at install time to validate the event exists in the dep's manifest (Emits list) and to wire up routing.
ms.DependsOn("@bob/orders@^1", func(n *ms.Need) {
n.Event("order_placed")
})
func (*Need) Table ¶
Table records a bare relation name from the dep's per-app tables as a SELECT request — the physical target is app_<id>."<prefix><table>" in the shared app tenant schema, not a cross-app mod_<id> schema. The catalog validates the name against the dep's exposed tables at install time; after app-owner approval, the platform issues GRANT SELECT against this consumer's per-app DB role.
ms.DependsOn("@anna/oauth@^0.4.0", func(n *ms.Need) {
n.Table("oauth_users")
n.Table("recent_orders")
})
type Notification ¶
type Notification struct {
Title Label
Body Label
Icon string
Link string
Audience NotifyAudience
}
Notification is the module-facing shape passed to Notify. Title and Body are i18n Labels (Text literal or T catalog key) resolved to per-locale maps AT SEND TIME, so the platform — not the module — picks the recipient's locale. Title is required; Body, Icon and Link are optional. An unset Audience defaults to NotifyAdmins.
type NotifyAudience ¶
type NotifyAudience string
NotifyAudience selects WHO inside the app receives a notification. The value travels on the wire (the envelope's "audience" field), so the strings are a SHARED CONTRACT with the dispatch's audience whitelist.
const ( // NotifyAdmins targets the app's admins only — the default when a // Notification leaves Audience unset. NotifyAdmins NotifyAudience = "admins" // NotifyAllMembers targets every member of the app. NotifyAllMembers NotifyAudience = "members" )
type OnEventOption ¶
type OnEventOption func(*onEventOptions)
OnEventOption configures an OnEvent registration. Today its only producer is ms.OptionalDependOn, which co-locates an optional cross-module dep declaration with the event handler that uses it.
func OptionalDependOn ¶
func OptionalDependOn(spec string, configure ...func(*Need)) OnEventOption
OptionalDependOn declares an OPTIONAL dependency. Returns an OnEventOption that gets passed as the third+ argument of ms.OnEvent — the dep is "scoped" to that event handler. If the dep isn't installed, the event source doesn't exist, the event never fires, the handler never runs; the missing dep is harmless.
Same spec syntax and Need-callback shape as DependsOn:
ms.OnEvent("@anna/billing/payment", onPayment,
ms.OptionalDependOn("@anna/billing@^1", func(n *ms.Need) {
n.Table("invoices")
}))
Repeated declarations of the same dep ID merge — Tables and Events accumulate across calls (set union); if the same ID is also declared required via DependsOn elsewhere, required wins for the optional flag, but Tables/Events still merge.
type PermanentTaskError ¶
type PermanentTaskError struct{ Err error }
PermanentTaskError marks a handler failure as non-retryable while preserving errors.Is/errors.As through Unwrap.
func (*PermanentTaskError) Error ¶
func (e *PermanentTaskError) Error() string
func (*PermanentTaskError) Retryable ¶
func (e *PermanentTaskError) Retryable() bool
func (*PermanentTaskError) Unwrap ¶
func (e *PermanentTaskError) Unwrap() error
type PermissionOpts ¶
type PermissionOpts struct {
DefaultRole roles.Role
CustomRoles []string
Label Label
Description Label
}
PermissionOpts configures a permission declared via RegisterPermission.
DefaultRole is the role the permission lands on (commonly roles.Viewer(); roles.Admin() keeps it admin-only). Admin is ALWAYS implicit — it is never stored in the role set. CustomRoles are extra role keys beyond the default.
Label and Description are optional display strings, built from ms.Text / ms.T. They are resolved against the module's i18n catalogs at registration and folded into the manifest (per-locale), so the platform's roles UI can show a localized human label instead of the raw permission key. A zero Label is omitted entirely.
type Res ¶
type Res struct {
// VCPU is honoured by Heavy only, and must form a legal pair with MemoryMB
// (see fargateSizes). Standard rejects it — Lambda DERIVES vCPU from memory
// and does not expose it as a dial. GPU rejects it too: the instance type
// fixes it.
VCPU float64
// MemoryMB is honoured by Standard (Lambda's 128..10240 MB) and Heavy
// (Fargate's matrix). GPU rejects it — the instance type fixes it.
//
// AWS's own Fargate table calls these values MB while metering MiB; the
// numbers here are the ones the ECS API takes, so the same convention is
// kept rather than silently converted.
MemoryMB int
// Instance is honoured by GPU only, and must name an allowlisted type (see
// gpuInstances). Standard and Heavy reject it: neither Lambda nor Fargate
// is instance-shaped, so accepting one would imply a control the platform
// cannot honour.
Instance string
}
Res is the resource request for a task's execution class.
Go has no keyword arguments, so a named-field struct is the closest readable form to Heavy(cpu=4, ram=8192). One struct serves all three classes, but each class honours a DIFFERENT subset — the fields a class does not honour are rejected rather than ignored, because silently dropping a resource request is how an author ends up paying for compute they did not get.
type SelectFilter ¶
SelectFilter is one WHERE predicate: Column = value (one Values entry) or Column IN (...) (several). Values are always bound as $n parameters.
type TaskHandler ¶
type TaskHandler func(ctx context.Context, payload json.RawMessage) error
TaskHandler handles one platform-managed background task attempt. Returning an ordinary error requests a retry according to the declared retry policy.
type TaskJobStatus ¶
type TaskJobStatus struct {
JobID string `json:"jobId"`
Status string `json:"status"`
Cancelled bool `json:"cancelled"`
}
TaskJobStatus is the current state of a task job. In CLI dev mode it comes from the process-local executor; deployed calls read platform-owned state.
func TaskStatus ¶
func TaskStatus(ctx context.Context, jobID string) (TaskJobStatus, error)
TaskStatus returns a local or managed task's current state on the default Module.
type TaskOption ¶
type TaskOption func(*taskOptions)
TaskOption configures task registration. See WithTimeout.
func WithCompute ¶
func WithCompute(c Compute) TaskOption
WithCompute selects the execution class and size for this task, so the platform provisions the right managed runner from the declaration alone.
Omitting it keeps the task on Standard/Lambda with platform-default sizing: the default must never move, or a module could land on expensive compute without anyone declaring it.
The descriptor must come from Standard, Heavy or GPU — those validate it. A zero Compute is a programmer error and panics rather than reaching the manifest as a runner class the platform cannot resolve.
func WithEphemeralStorage ¶
func WithEphemeralStorage(sizeBytes int64) TaskOption
WithEphemeralStorage declares Heavy/Fargate runner-local scratch storage. Heavy tasks accept 20 GiB (the implicit default, omitted from the manifest) or a whole-GiB value from 21 through 200 GiB. Standard/Lambda and GPU tasks do not expose this option; GPU host scratch is platform-owned and fixed.
func WithMaxRetries ¶
func WithMaxRetries(n int) TaskOption
WithMaxRetries sets the maximum number of platform-managed retry attempts.
func WithTimeout ¶
func WithTimeout(d time.Duration) TaskOption
WithTimeout sets the maximum duration for this task handler.
type UIComponent ¶
type UIComponent = registry.UIComponent
Public type aliases for the UI manifest. The data shapes live in the registry package (alongside the storage methods); core re-exports them so module authors construct ms.ModuleUI{...} without importing internal/.
Source Files
¶
- audit_delivery.go
- call.go
- compute.go
- contribute.go
- cron.go
- db.go
- db_guard.go
- db_placeholder.go
- delivery.go
- dependency_db.go
- dependency_local.go
- describe.go
- dev_directory.go
- dev_migrate.go
- dispatch_transport.go
- emit.go
- event.go
- expose.go
- lambda_shim.go
- local_task.go
- logging.go
- mcp.go
- member_assertion.go
- module.go
- module_config.go
- module_contributions.go
- module_default.go
- module_permissions.go
- module_scopes.go
- module_start.go
- module_system_routes.go
- need.go
- notify.go
- readme.go
- release_manifest_tool.go
- resources.go
- schema.go
- select.go
- task.go
- ui.go