mirrorstack

package module
v0.4.17 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

@mirrorstack-ai/app-module-sdk

Good First Issues

Go SDK for building modules on MirrorStack, the Agentic CMS platform.

Built with Go, chi router, and designed for AWS Lambda + local dev.

Docs | Template module | Changelog | Issues | Good First Issues | Slack

Status: Under active development — see the roadmap below.

The reference/v2-restored branch contains the original implementation (handler, event, meter, storage packages with tests) being restructured into the new configless design.

Getting started

Install
go get github.com/mirrorstack-ai/app-module-sdk
Usage
package main

import (
    "github.com/go-chi/chi/v5"
    ms "github.com/mirrorstack-ai/app-module-sdk"
    "github.com/mirrorstack-ai/app-module-sdk/db"
)

func main() {
    ms.Init(ms.Config{
        ID:   "media",
        Name: "Media",
        Icon: "perm_media",
    })

    ms.Platform(func(r chi.Router) {
        r.Get("/items", listItems)
        r.Post("/items", createItem)
    })

    ms.Start()
}

func listItems(w http.ResponseWriter, r *http.Request) {
    conn, release, err := ms.DB(r.Context())
    if err != nil { ... }
    defer release()

    queries := generated.New(conn) // sqlc-generated, type-safe
    items, err := queries.ListItems(r.Context(), params)
}

func createItem(w http.ResponseWriter, r *http.Request) {
    err := ms.Tx(r.Context(), func(q db.Querier) error {
        queries := generated.New(q)
        // reads + writes inside a transaction — atomic
        return queries.CreateItem(r.Context(), params)
    })
}

No config files. No YAML. Code is the single source of truth.

Module client

A module can declare a separately built client for custom web applications:

import "github.com/mirrorstack-ai/app-module-sdk/system"

ms.Init(ms.Config{
    ID:   "media",
    Name: "Media",
    Client: &system.ClientSpec{
        Dir:       "client",
        OutputDir: "dist",
    },
})
  • Paths: Dir is relative to the module root; OutputDir is relative to Dir. Both are non-empty canonical relative paths using / separators.
  • Declaration: use the inline literal above because the CLI reads it before the module starts.
  • Build: install dependencies locally for the runner's Linux architecture and provide a non-empty package.json#scripts.build. The CLI runs npm run build and requires non-empty index.js and index.d.ts roots.
  • Development: mirrorstack dev --tunnel builds, publishes, and watches by default; the explicit mirrorstack dev --tunnel --watch form behaves the same. Use --watch=false for a one-shot build. --share is not required.
  • Runner: Compose must use a Linux runner built from the matching CLI checkout. In ms-app-modules, run ./scripts/build-dev-runner.sh /path/to/mirrorstack-cli once and after CLI changes; preflight rejects a missing/non-executable runner or an incompatible CLI version or runner protocol. It does not fingerprint same-version source changes.
  • Ownership: MirrorStack owns installed package identity, version, registry, and the fixed build command; modules declare only source and output paths.
Struct API (for testing)
mod, err := ms.New(ms.Config{ID: "media", Name: "Media", Icon: "perm_media"})

mod.Platform(func(r chi.Router) { ... })
mod.Start()

Runtime

ms.Start() auto-detects the environment. One binary, works everywhere:

Environment What happens How platform calls
Local dev HTTP server on :8080 HTTP proxy
AWS Lambda Lambda handler Lambda Invoke SDK (VPC-internal)
Managed Heavy/GPU task Runs one broker-claimed attempt, then exits Protected one-shot runner handoff

Port defaults to 8080. Override with PORT env var.

Warning: never expose the dev-mode HTTP server to the public internet.

When AWS_LAMBDA_FUNCTION_NAME is unset, InternalAuth returns 401 (not 503) on a missing secret to keep local tooling friendly. A dev server bound to 0.0.0.0 with no MS_INTERNAL_SECRET set will accept platform calls from anyone who can reach the port. Use Lambda or the managed one-shot task plane for production. For staging, set MS_INTERNAL_SECRET and put the server behind a VPN or IP allowlist.

Database

ms.DB(ctx) returns a scoped database connection with automatic tenant isolation:

conn, release, err := ms.DB(r.Context())
defer release()
// queries run against this app's schema only

ms.Tx(ctx, fn) runs operations in a transaction — atomic commit or rollback:

err := ms.Tx(r.Context(), func(q db.Querier) error {
    // all reads + writes here are atomic
    return nil // commit, or return error to rollback
})
Module-shared schema (mod_<id>)

For state that crosses app boundaries — outbox tables, dedup ledgers, rate limiters, module-wide config — use ms.ModuleDB and ms.ModuleTx:

// Insert into the module's shared outbox table
err := ms.ModuleTx(r.Context(), func(q db.Querier) error {
    return q.Exec(ctx, "INSERT INTO outbox (event, payload) VALUES ($1, $2)", ...)
})

ModuleDB and ModuleTx operate on the module's mod_<id> schema — independent of the per-app app_<id> schema that DB/Tx use. A handler that needs both calls both — they use independent credentials and don't interfere.

Do not build a module-shared audit log. Record a business change with audit.Record inside the same ms.Tx; the SDK owns the per-app durable outbox, post-commit delivery, retry and provenance handoff. ms.DrainAudit is the shared backlog hook for cron/worker execution.

The mod_<id> schema is where you put sql/module/*.up.sql migrations (see Module structure below).

Platform role grant contract

The SDK relies on the platform provisioning two distinct DB roles per deployed module with disjoint privileges:

Role Schema Granted via
Per-(module, app) app_<appID> only injected as db.Credential for Module.DB / Module.Tx
Per-module mod_<moduleID> only injected as db.Credential for Module.ModuleDB / Module.ModuleTx

Cross-credential contamination (e.g., a misrouted app credential reaching ModuleDB) is caught by Postgres at the SQL layer because the per-(module, app) role lacks USAGE on mod_<id> — defense-in-depth, no SDK enforcement required. Verify your platform-side role provisioning keeps the two grant sets disjoint.

Security (3-layer isolation)
Layer What How
Credentials Each (module, app) pair gets its own DB role Platform injects per-invocation via Lambda payload
Schema SET search_path per request SDK sets/resets automatically
RLS Row-level policies on app_id column Defense-in-depth, platform-managed

Module developers don't think about any of this — just call ms.DB(ctx).

Auth & permissions

Three scopes control who can call your routes:

Scope Who Default
ms.Platform() Dashboard users (authenticated) Any authenticated role
ms.Public() Anyone (end users, anonymous) No auth check
ms.Internal() Platform only (events, cron) Validates MS_INTERNAL_SECRET
Permissions

Use ms.RequirePermission for fine-grained role control. Roles are typed values from the roles package — import aliased as p by convention:

import p "github.com/mirrorstack-ai/app-module-sdk/roles"

ms.Platform(func(r chi.Router) {
    r.With(ms.RequirePermission("media.view",   p.Admin(), p.Viewer())).Get("/items", listItems)
    r.With(ms.RequirePermission("media.upload", p.Admin())).Post("/items", uploadItem)
    r.With(ms.RequirePermission("media.moderate", p.Custom("moderator"))).Post("/flag", flagItem)
})

Canonical roles: p.Admin(), p.Viewer(). Use p.Custom("key") for module-specific roles. Typed values prevent typos and enable IDE autocomplete.

Permissions are auto-registered for manifest generation — the platform knows what each module requires.

Context helpers
func handler(w http.ResponseWriter, r *http.Request) {
    a := auth.Get(r.Context())
    a.UserID   // who
    a.AppID    // which app
    a.AppRole  // what access (admin, member, viewer)
}
Query approach

sqlc as default — write SQL, generate type-safe Go:

-- sql/queries/media.sql
-- name: ListItems :many
SELECT id, title FROM media_items ORDER BY created_at DESC LIMIT $1 OFFSET $2;
queries := generated.New(conn) // sqlc-generated
items, err := queries.ListItems(ctx, params) // type-safe, no manual scanning

Raw pgx as escape hatch — for dynamic queries:

conn.Query(ctx, "SELECT * FROM items WHERE title ILIKE $1", "%"+search+"%")

Cache

ms.Cache(ctx) returns a scoped Redis client with automatic key prefix isolation:

c, err := ms.Cache(r.Context())
if err != nil { ... }

c.Set("views:123", "42", 5*time.Minute)   // Redis key: app_abc:mod_media:views:123
val, err := c.Get("views:123")             // returns "42"
c.Del("views:123")

Keys auto-prefixed: developer writes "views:123", Redis stores "app_abc123:mod_media:views:123". Each app sees only its own cache.

Roadmap

Core
  • ms.Init() / ms.New() — module registration
  • ms.Start() — runtime auto-detection (HTTP server / Lambda handler)
  • ms.Platform() / ms.Public() / ms.Internal() — auth scopes with middleware
  • ms.RequirePermission() — per-route permission with auto-registration for manifest (typed roles: roles.Admin(), roles.Viewer(), roles.Custom("..."))
Database
  • ms.DB(ctx) — scoped connection with schema-per-app isolation
  • ms.Tx(ctx, fn) — atomic transactions
  • Per-app credential pools (PoolCache with LRU eviction)
  • Schema leak prevention (sanitize before borrow, destroy dirty connections)
  • RLS support (ms.app_id session variable)
  • sqlc-compatible Querier interface
Platform resources
  • ms.RequireStorage() + ms.Storage(ctx) — explicit startup declaration, then per-request S3 presigned URLs + CDN (R2 cache) + multipart upload
  • ms.Cache(ctx) — scoped Redis (ElastiCache Serverless)
  • ms.Meter(ctx) — custom usage metrics for billing
Events & scheduling
  • ms.OnEvent() — subscribe to events from other modules
  • ms.Emits() — declare emitted events
  • ms.Cron() — register scheduled jobs
System routes (/__mirrorstack/)
  • health — health check
  • platform/manifest — module identity + capabilities
  • mcp/tools/{list,call} and mcp/resources/{list,read} — agent tool and resource surface
  • platform/lifecycle/app/install and platform/lifecycle/module/install — fresh install (per-app schema or per-module shared schema)
  • platform/lifecycle/app/upgrade and platform/lifecycle/module/upgrade — upgrade between versions
  • platform/lifecycle/app/downgrade and platform/lifecycle/module/downgrade — rollback between versions
  • platform/lifecycle/app/uninstall and platform/lifecycle/module/uninstall — soft-delete removal
Agent orchestration
  • ms.Describe() — module description for agent discovery
  • ms.DependsOn() — dependency declaration with auto-detected required/optional
  • ms.ExposeTable() — mark a table read-only (SELECT) eligible for depending modules; app owner approves who reads (no producer allowlist)
  • ms.Resolve[T]() — typed runtime lookup for optional deps (stub pending cross-module wiring)
  • ms.MCPTool() — agent-callable tool with JSON Schema derivation
  • ms.MCPResource() — agent-readable resource
  • /__mirrorstack/mcp/ — MCP protocol routes (tools/resources, list/call/read)

SDK structure

app-module-sdk/
  mirrorstack.go               Facade: type aliases + wrapper functions for core package
  internal/core/
    module.go                  *Module type, lifecycle, route registration
    db.go                      DB/ModuleDB/Tx/ModuleTx implementation
    describe.go                Describe, dependency declaration + registry
    mcp.go                     MCPTool, MCPResource implementation
    cron.go                    Cron job registration + scheduling
    event.go                   OnEvent, Emits event wiring
    task.go                    OnTask, RunTask for managed background tasks
    resources.go               System routes (manifest, health, lifecycle)
    [test files]               Unit + integration tests
  internal/
    httputil/respond.go        JSON response helper
    runtime/
      detect.go                Lambda detection
      lambda.go                Lambda handler, credential injection
    [other internal pkgs]      Registry, migration, IDs, task env, etc.
  auth/
    context.go                 WithUserID/WithAppID/WithAppRole, role constants
    middleware.go              PlatformAuth, PublicAuth, InternalAuth
    permission.go              RequireRoles middleware (no tracking — see Module.RequirePermission)
  db/
    credential.go              Credential struct, context helpers
    db.go                      Dev-mode client, Querier interface
    pool_cache.go              PoolCache (LRU), AcquireScoped, prepareConnReset
    scope.go                   applyScope (batch SET)
    tx.go                      Transaction support
    [test files]               Unit + integration tests
  system/
    health.go                  Health endpoint
  cache/
    credential.go              Cache credential, context helpers
    cache.go                   Client with Set/Get/Del, key prefix, ForApp
  storage/
    credential.go              STS credential, context helpers
    storage.go                 Client with PresignPut/Get, URL (CDN)
    multipart.go               Multipart upload for large files
  meter/
    meter.go                   Custom usage metrics recording
  mcp/                         (unused — MCP impl is in internal/core/mcp.go)
  roles/
    roles.go                   Role types for permission declarations

Module structure

app-mod-{name}/
  sql/
    app/                       Per-app migrations (one app_<id> schema per tenant)
      0000_initial.up.sql
      0000_initial.down.sql
    module/                    Per-module migrations (single mod_<id> shared schema)
      0000_outbox.up.sql       e.g. module-wide outbox or dedup ledger
      0000_outbox.down.sql
    queries/                   sqlc query definitions
  api/                         Go backend
    cmd/main.go
    handler/
      platform/                Dashboard routes
      public/                  End-user routes
      internal/                Events, cron
    service/                   Business logic
  web/                         React frontend (Module Federation)
    platform/pages/
    app/pages/

Development

# Start local postgres
docker compose up -d

# Run unit tests
go test ./...

# Run integration tests (requires postgres)
go test -tags integration ./...

Tech stack

  • Go 1.26 with chi v5 router
  • AWS Lambda via auto-detection
  • PostgreSQL with schema-per-app isolation (Aurora Serverless v2)
  • pgx v5 for database access, sqlc for query generation
  • S3 + Cloudflare R2 for storage (S3 primary, R2 CDN cache)
  • Redis via ElastiCache Serverless
  • MCP for AI agent integration

Contributing

We welcome contributions! See CONTRIBUTING.md to get started.

Look for issues labeled good first issue for beginner-friendly tasks.

License

Apache-2.0. Earlier releases were published under FSL-1.1-ALv2.

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. All implementation lives in internal/core; this file is a re-export facade — type aliases for Config and Module, and package-level wrapper functions that forward to the same-named symbols in core.

Index

Constants

View Source
const (
	MiB = core.MiB
	GiB = core.GiB
)
View Source
const (
	Counter = meter.Counter
	Gauge   = meter.Gauge
	// BySubject makes a Gauge bill as SUM(MAX(value) per Observation.Subject)
	// within each billing period. It is invalid with Counter or reserved metrics.
	BySubject = meter.BySubject
)

Counter and Gauge are the metric-kind options passed to Meter. Counter is additive (the platform SUMs it); Gauge is an absolute level (MAX or a time-weighted integral, never summed).

They are CONSTANTS, not vars, so a third-party module cannot reassign ms.Counter / ms.Gauge (the SDK is a trust boundary). The call site stays parens-free: ms.Meter(name, ms.Counter, ...).

View Source
const (
	NotifyAdmins     = core.NotifyAdmins
	NotifyAllMembers = core.NotifyAllMembers
)

NotifyAdmins and NotifyAllMembers are the notification audiences. They are CONSTANTS, not vars, so a third-party module cannot reassign them (the SDK is a trust boundary).

View Source
const (
	UISurfaceMain     = core.UISurfaceMain
	UISurfaceSettings = core.UISurfaceSettings
)

Known UIPage.Surface values. Empty (UISurfaceMain) is the default — pages mount at /apps/<app>/<module-slug>/<route>. UISurfaceSettings mounts pages at /apps/<app>/settings/module/<module-slug>/<route> instead, for per-module configuration UIs.

View Source
const GatedKeyPrefix = core.GatedKeyPrefix

GatedKeyPrefix is the module-relative storage-key prefix that opts an object into token-gated CDN delivery. Write gated objects under it (and store that same key), then hand viewers a URL from ms.DeliveryURL or ms.Delivery.

Objects NOT under it are served by the CDN with no credential at all, so this prefix is the whole difference between a public asset and a paywalled one.

Variables

View Source
var (
	// ErrDependencyUnauthorized — the platform could not authenticate the
	// read (no live dev-tunnel session / wrong service secret / module not
	// in the app). Re-establish the tunnel session.
	ErrDependencyUnauthorized = core.ErrDependencyUnauthorized
	// ErrNotExposed — the table is not exposed to this module by the version
	// the producer actually runs, or the app owner never consented.
	//
	// DEV DOES NOT CHECK CONSENT, so this is the error most likely to appear
	// for the first time in production. Under `mirrorstack dev` a read of a
	// co-located producer is authorized by your ms.DependsOn declaration plus
	// the producer's ms.ExposeTable — that is all there is locally, because a
	// dev session has no install lifecycle and therefore no app owner to
	// approve anything. In production those two are necessary but NOT
	// sufficient: the app owner must also have consented at install time, and
	// exposure is anchored to the version the producer provably runs in that
	// app rather than to your working tree. A consumer that reads cleanly all
	// the way through dev can still be refused on install. Handle
	// ErrNotExposed as a live runtime outcome, not a setup mistake.
	ErrNotExposed = core.ErrNotExposed
	// ErrDependencyUnavailable — authorized, but the producer's relation is
	// not readable right now (yanked/rolled back), or the platform's read
	// proxy is disabled.
	ErrDependencyUnavailable = core.ErrDependencyUnavailable
	// ErrProducerNotFound — the producer ref does not resolve to an install
	// in this app.
	ErrProducerNotFound = core.ErrProducerNotFound
)

Typed failure modes for dependency reads — match with errors.Is. All fail-closed: a dependency read NEVER silently returns empty rows for an authorization or availability failure.

View Source
var ErrCallResponseTooLarge = core.ErrCallResponseTooLarge

ErrCallResponseTooLarge is returned when a successful response exceeds CallOptions.MaxResponseBytes. Use errors.Is to match it.

Functions

func AbsorbInfra

func AbsorbInfra()

AbsorbInfra 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 your module bills through its OWN meters and adds no infrastructure passthrough on top.

One flag, no metric names: the platform expands the set from its own catalog at publish, so a metric introduced after your module shipped is absorbed without a republish. An explicit ms.Meter("infra.X", ms.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

func AppID

func AppID(ctx context.Context) string

AppID returns the current app id from the request context, or "" if no identity is set. It is the inbound twin of WithAppID and the single unspoofable way a handler reads its OWN app.

On every guarded surface the SDK promotes the platform's trusted, dispatch- injected app id into the context identity BEFORE the handler runs — Platform via PlatformAuth, Public via the proxy guard's validated-token path, and Lambda via the typed invoke payload (runtime.InjectResources). So a Public or Platform handler reads its app with:

appID := ms.AppID(r.Context())

Do NOT read the app id from request data (query string, body, path) — those are caller-controlled and forgeable — and do NOT read the auth.HeaderAppID header: the deployed Lambda shim strips it, so header reads silently break deployed (ms-app-modules#30; see the auth.Header* docs). ms.AppID is the trusted source on every path.

func AppRole

func AppRole(ctx context.Context) string

AppRole returns the current user's role in the app from the request context ("admin", "member", or "viewer" — compare against auth.RoleAdmin / auth.RoleMember / auth.RoleViewer), or "" if no identity is set. Identity promotion and the always-the-context-never-the-X-MS-*-headers rule are exactly as documented on ms.UserID. Note: ms.AppRole is a read of WHO the platform says the caller is — for gating routes by role, prefer the declarative RequirePermission middleware:

if ms.AppRole(r.Context()) == auth.RoleAdmin { ... }

"" is a legitimate value: internal/system/cron/task invocations carry no user role. Under the local-dev bypass (no secret configured) it returns the synthetic auth.RoleAdmin.

func Cache

func Cache(ctx context.Context) (cache.Cacher, func(), error)

Cache returns a scoped cache client on the default module.

func Call

func 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, scoped to the current app (app id read from ctx via the SDK's auth identity — callers don't pass it). It JSON-marshals body, sends method to the target module's path, and decodes the JSON response into out (pass nil body for GET, nil out to ignore the response). path must include its leading slash and any raw query, e.g. "/internal/users?limit=10".

The caller never holds the callee's credentials: dispatch injects the TARGET module's per-session token + identity before forwarding. Declare the target as a dependency (ms.DependsOn) — inter-module calls without a declared dep are a wiring bug the platform enforces.

Dev/dispatch transport today; prod catalog/Lambda endpoint resolution is the documented #146 seam (see core.resolveCallURL).

func CallDependencyPost

func CallDependencyPost(ctx context.Context, producerRef, path string, body, out any) error

CallDependencyPost sends an actorless POST to a declared dependency's Internal surface. producerRef accepts the same stable ref used by DependsOn.

func CallGet

func CallGet(ctx context.Context, targetModuleID, path string, out any) error

CallGet is Call specialized to GET (no request body) on the default module.

func CallPost

func CallPost(ctx context.Context, targetModuleID, path string, body, out any) error

CallPost is Call specialized to POST with a JSON body on the default module.

func CallWithOptions

func CallWithOptions(ctx context.Context, targetModuleID, method, path string, body, out any, options CallOptions) error

CallWithOptions is Call with opt-in bounded and strict response decoding. A negative MaxResponseBytes is rejected before any network I/O.

func CancelTask

func CancelTask(ctx context.Context, jobID string) error

CancelTask requests monotonic, idempotent cancellation of a local or managed task.

func ContributesTo

func ContributesTo(hostSpec, slot string, payload any)

ContributesTo declares that this module pushes payload into a host module's slot — the contributor side of Provide. hostSpec is an id or id@constraint; see DependsOn for the full syntax. Zero-runtime: it becomes manifest metadata and the platform (CLI in dev) performs registration after app-owner approval.

func Cron

func Cron(name, schedule string, handler http.HandlerFunc)

Cron registers a scheduled job on the default module.

func DB

func DB(ctx context.Context) (db.Querier, func(), error)

DB returns a scoped database connection on the default module.

func DeliveryURL

func DeliveryURL(ctx context.Context, key string, ttl time.Duration) (string, error)

DeliveryURL returns the CDN URL for ONE gated object, with a freshly minted, short-lived delivery credential attached. key is relative to this module's storage prefix (the same key space ms.Storage uses) and must live under ms.GatedKeyPrefix. ttl is a proposal the platform clamps.

Call it only AFTER deciding the viewer is entitled. The module owns that decision — was the credit charged, is the role held — and the platform owns the credential; the signing key never reaches the module.

🔴 An error means NO URL. It never degrades to an unsigned CDN URL: that would serve paywalled bytes to anyone with the link while looking like a working feature, and those URLs end up in viewers' playlists where a rollback cannot reach them.

One mint per call. For many objects under one prefix — a video's segments — use ms.Delivery once and reuse the ticket.

func DependsOn

func DependsOn(spec string, configure ...func(*Need))

DependsOn declares a REQUIRED dependency on the default module. The optional configure callback names what the consumer wants from the dep — relations (n.Table) and events (n.Event). See the core package for the full spec syntax (id or id@constraint).

func DrainAudit

func DrainAudit(ctx context.Context) error

DrainAudit forwards one bounded batch from the current app's durable audit outbox. Tx already attempts this after a successful commit; cron and worker handlers can call DrainAudit to work through older backlog without owning lease, retry, or transport code themselves.

func Emit

func Emit(ctx context.Context, name string, payload any) error

Emit publishes an event to every LIVE module that subscribes to name within the current app, scoped to the current app (app id read from ctx via the SDK's auth identity — callers don't pass it). The SDK wraps payload in the event envelope ({id, name, sourceModuleID, sentAt, payload}) and POSTs it to the platform dispatch event bus, which fans the SAME envelope out to each subscriber session. An empty app-scope context is an error (no panic).

Delivery is AT-LEAST-ONCE and best-effort per subscriber — subscriber handlers (ms.OnEvent) must be idempotent. Declare the event vocabulary with ms.Emits so subscribers can discover it.

Dev/dispatch transport today; prod event-bus endpoint resolution is the documented #146 seam (see core.resolveEventBusURL, mirroring core.resolveCallURL).

func Emits

func Emits(name string)

Emits declares that the default module emits the named event.

func ExposeTable

func ExposeTable(name string)

ExposeTable marks a table in this module's schema as read-only SELECT-eligible for a depending module — the producer side of DependsOn's n.Table. It is a zero-runtime DECLARATION that lands in the manifest under exposes.tables; the platform issues GRANT SELECT after the app owner approves a dependency.

The producer only opts a table IN to being readable — it does NOT decide WHO reads it. The app owner (not the producer) is the trust root and chooses which installed modules may read by approving their dependency. There is intentionally NO consumer allowlist: in a marketplace the consumers are third parties, so a publisher-controlled reader list is the wrong model. v1 is TABLES ONLY, read-only (SELECT). Panics on an empty or invalid table identifier. Call from startup code.

func Init

func Init(cfg Config) error

Init creates the default Module and stores it for the package-level convenience wrappers to dispatch to.

func Internal

func Internal(fn func(r chi.Router))

Internal registers internal-scoped routes on the default module.

func IsPermanent

func IsPermanent(err error) bool

IsPermanent reports whether err contains a permanent task wrapper.

func Log

func Log(ctx context.Context) *slog.Logger

Log returns the request's structured logger, pre-tagged with the trusted app_id / request_id / module_id correlation fields so every line is attributable in the platform Logcat. Prefer it over the standard library `log` for all module logging. Outside a request it returns the process default logger.

ms.Log(r.Context()).Info("user signed in", "provider", "google")

func MCPResource deprecated

func MCPResource[Out any](name, description string, handler func(ctx context.Context) (Out, error))

MCPResource registers an agent-readable resource on the default module.

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 with JSON Schema derived from the type parameters via reflection. Optional MCPToolOptions scope the tool, e.g. ms.ToolPermission("users.read").

func Meter

func Meter(name string, opts ...MetricOption)

Meter DECLARES a usage metric on the default module. Call it ONCE per metric in startup code (exactly like ms.Emits / ms.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.

The kind is an OPTION: pass ms.Counter (additive; platform SUMs) or ms.Gauge (absolute level; platform takes MAX or a time-weighted integral). Emit at runtime BY NAME with ms.Record(ctx, name, value) — mirroring ms.Emits/ms.Emit; the platform reads the declared kind from the catalog, so a call site can never mislabel a metric.

Add ms.BySubject to a Gauge when the billing quantity is SUM(MAX(value) per end-user subject), and emit it with RecordObservation. A custom metric MUST pass exactly one kind option. A reserved infra.*/platform.* metric is platform-measured: it may carry ms.Price ONLY (to override the customer passthrough) — passing a kind, aggregation, or unit on it panics, as does a duplicate name, an invalid name, or conflicting kinds.

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)) // absorb platform compute

To absorb EVERY platform infra metric rather than name them one at a time, call AbsorbInfra — a metric name written here is a copy of the platform's catalog and goes stale when the platform renames one.

func ModuleDB

func ModuleDB(ctx context.Context) (db.Querier, func(), error)

ModuleDB returns a connection scoped to the module's shared schema.

func ModuleTx

func ModuleTx(ctx context.Context, fn func(q db.Querier) error) error

ModuleTx runs fn inside a transaction scoped to the module's shared schema.

func Notify

func Notify(ctx context.Context, n Notification) error

Notify sends an in-app notification to the current app's members, scoped to the current app (app id read from ctx via the SDK's auth identity — callers don't pass it). The SDK resolves the Notification's Labels, wraps them in the notification envelope ({id, sentAt, sourceModuleID, title, body, icon, link, audience}) and POSTs it to the platform dispatch notification ingress, which re-derives the sender identity from the live session and writes the notification into the app's feed. An empty app-scope context, an unset Title, or an unknown Audience is an error (no panic).

ms.Notify(r.Context(), ms.Notification{
    Title:    ms.T("notifications.order.placed"),
    Link:     "/orders/42",
    Audience: ms.NotifyAllMembers,
})

Dev/dispatch transport today; prod notification-ingress resolution is the documented #146 seam (see core.resolveNotifyURL, mirroring core.resolveEventBusURL).

func OnEvent

func OnEvent(name string, handler http.HandlerFunc, opts ...OnEventOption)

OnEvent subscribes the default module to an event from another module. Optional cross-module deps can be co-located via OptionalDependOn:

ms.OnEvent("@anna/billing/payment", onPayment,
    ms.OptionalDependOn("@anna/billing@^1", func(n *ms.Need) {
        n.Table("invoices")
    }))

func OnTask

func OnTask(name string, handler TaskHandler, opts ...TaskOption)

OnTask registers a background task handler on the default module.

func Permanent

func Permanent(err error) error

Permanent marks a task failure as non-retryable.

func Platform

func Platform(fn func(r chi.Router))

Platform registers platform-scoped routes on the default module.

func Provide

func Provide[T any](key string)

Provide declares an extension slot others contribute to on the default module. The type parameter T fixes the payload shape — incoming register requests must unmarshal cleanly into T. The SDK auto-mounts:

POST   /__mirrorstack/contrib/<key>/<module-uuid>   register/upsert
DELETE /__mirrorstack/contrib/<key>/<module-uuid>   unregister
GET    /__mirrorstack/contrib/<key>                 list all registered

Each is Internal-scoped (HMAC-gated by the SDK). Host modules that want to expose a Platform-scoped read with permission gating wrap these endpoints in their own handler — see oauth-core's /platform/providers for the pattern.

Panics on duplicate key or before Init — matches RegisterUI / RequirePermission startup-error conventions.

type ProviderContribution struct {
    Name         string `json:"name"`
    Icon         string `json:"icon"`
    LoginPath    string `json:"login_path"`
    CallbackPath string `json:"callback_path"`
}

ms.Provide[ProviderContribution]("providers")

func Public

func Public(fn func(r chi.Router))

Public registers public-scoped routes on the default module.

func Record

func Record(ctx context.Context, name string, value float64) error

Record emits a usage event for the metric DECLARED (via ms.Meter) under name — BY NAME, exactly mirroring ms.Emit. The platform reads the declared kind from its catalog to decide how to aggregate, so the call site never repeats the kind.

Declaration-first: Record returns an error if name was never declared via ms.Meter (fail fast). It also returns an error (never panics) if value is negative or non-finite. A billing failure must NEVER fail the handler — log the error, don't propagate it.

if err := ms.Record(r.Context(), "orders.placed", 1); err != nil {
    log.Printf("meter: %v", err) // don't fail the handler
}

func RecordObservation

func RecordObservation(ctx context.Context, name string, value float64, observation Observation) error

RecordObservation emits a v2 meter observation with a persisted EventID, opaque end-user Subject, bounded diagnostic Metadata, and original OccurredAt. Metrics declared BySubject must use this path. The SDK validates structure and normalizes OccurredAt to UTC; API and Billing authoritatively decide the accepted occurrence window, replay outcome, and billing period.

func RecordWithID

func RecordWithID(ctx context.Context, eventID, name string, value float64) error

RecordWithID emits a usage event with a caller-persisted UUID as its idempotency key. Persist the ID with the billable state change and reuse it across ambiguous delivery retries.

func RegisterMessages

func RegisterMessages(fsys fs.FS, dir string) error

RegisterMessages loads the module's i18n catalogs from fsys/dir (one <locale>.json per file, nested JSON flattened to dotted keys). Call once at startup, typically with a //go:embed of i18n/*.json. These catalogs back the Labels resolved by RegisterPermission and surfaced in the manifest.

//go:embed i18n/*.json
var i18nFS embed.FS
ms.RegisterMessages(i18nFS, "i18n")

func RegisterPermission

func RegisterPermission(name string, opts PermissionOpts)

RegisterPermission declares a module permission ONCE — its default role, custom roles, and resolved i18n Labels/Descriptions — recording it in the manifest. Call at startup (typically a declare/ package) BEFORE the routes that gate on it. First-wins by name. Names are slug-qualified automatically.

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 RegisterUI

func RegisterUI(ui ModuleUI)

RegisterUI declares the module's UI surface (agent-visible Components plus DefaultPages). Panics on programmer errors (duplicate names, invalid slug, unknown prop type). Last-write-wins.

func RequirePermission

func RequirePermission(name string) func(http.Handler) http.Handler

RequirePermission returns Chi middleware gating a route on a permission previously declared via RegisterPermission, looked up by NAME:

ms.RequirePermission("users.read")

Admin always passes. If the name was never RegisterPermission'd it is registered lazily as admin-only (safe-by-default) with a dev warning.

func RequireStorage

func RequireStorage()

RequireStorage declares that this module needs its per-app object-storage namespace. Call it during startup before Start; the platform vends storage credentials only to modules whose manifest contains this declaration.

func Resolve

func Resolve[T any](id string) (T, bool)

Resolve looks up a typed client registered by another module. v1 stub always returns (zero, false).

func RunTask

func RunTask(ctx context.Context, name string, payload json.RawMessage) (string, error)

RunTask enqueues a task on the default module.

func RunTaskWithIdempotencyKey

func RunTaskWithIdempotencyKey(ctx context.Context, name string, payload json.RawMessage, idempotencyKey string) (string, error)

RunTaskWithIdempotencyKey creates or recovers one logical managed task job. Persist and reuse the UUID when retrying after an ambiguous enqueue result.

func Start

func Start() error

Start boots the default module: validates manifest, opens DB, binds routes, and starts the HTTP server or Lambda handler depending on runtime mode.

func Storage

func Storage(ctx context.Context) (storage.Storer, error)

Storage returns a scoped storage client on the default module.

func Tx

func Tx(ctx context.Context, fn func(q db.Querier) error) error

Tx runs fn inside a per-app transaction on the default module.

func UserID

func UserID(ctx context.Context) string

UserID returns the current user id from the request context, or "" if no identity is set. Together with ms.AppID and ms.AppRole (or auth.Get, which returns the full Identity in one read) it is the ONLY correct way for module code to read the request's identity: always the context, never the auth.HeaderUserID (X-MS-User-ID) header — the deployed Lambda shim strips it, so header reads work under the dev tunnel and silently break deployed (ms-app-modules#30; see the auth.Header* docs).

On every guarded surface the SDK promotes the platform's trusted, dispatch- injected user id into the context identity BEFORE the handler runs — Platform via PlatformAuth, Public via the proxy guard's validated-token path, and Lambda via the typed invoke payload (runtime.InjectResources). So a handler reads the requesting user with:

userID := ms.UserID(r.Context())

"" is a legitimate value, not only an unauthenticated-middleware artifact: internal/system/cron/task invocations carry no user, and an anonymous Public request may carry an identity whose user id is empty. Under the local-dev bypass (no secret configured) it returns the synthetic "local-dev-user".

func WithAppID

func WithAppID(ctx context.Context, appID string) context.Context

WithAppID returns a context whose inter-module Call scope is the given app, overriding the ambient identity's app. ms.Call reads the app id from the context (auth.Get) — for a handler that is the request's authenticated app, so authenticated callers need nothing extra.

Use it to RETARGET an outbound ms.Call at a DIFFERENT app than the ambient one (the rare cross-app proxy case). To read your OWN app id, use ms.AppID — the SDK already promotes the request's trusted app into the identity, so a Public/Platform handler does not need WithAppID just to call within its own app:

ctx = ms.WithAppID(ctx, otherAppID)
ms.CallGet(ctx, providerModuleID, "/internal/authorize-url?"+q, &out)

Any existing UserID/AppRole on the context is preserved; only AppID changes.

func WriteError

func WriteError(w http.ResponseWriter, status int, msg string)

WriteError writes the JSON error envelope {"error": msg} with the given status code, via WriteJSON. Use the canonical ErrorResponse shape rather than an ad-hoc map so module errors match the SDK's own error wire format.

func WriteJSON

func WriteJSON(w http.ResponseWriter, status int, v any)

WriteJSON writes v to w as JSON with the given status code and a Content-Type: application/json header. A nil v writes only the status and header with no body (for 204-style empty responses). An encode error is logged, not returned — the status line is already committed by then.

This is the response writer every module otherwise hand-rolls; it backs the same internal helper the SDK uses for its own endpoints.

func WriteServerError

func WriteServerError(w http.ResponseWriter, r *http.Request, operation string, err error)

WriteServerError logs err with the request's trusted correlation fields and returns a fixed, non-sensitive 500 response. operation is an internal log label; it is never copied to the response. Use this boundary instead of returning err.Error(), which can expose SQL, credentials, or topology.

Types

type CallOptions

type CallOptions = core.CallOptions

CallOptions opts one inter-module response into a byte bound and/or strict JSON decoding. Its zero value preserves Call behavior.

type Compute

type Compute = core.Compute

Compute is a validated execution-class descriptor. Obtainable only from Standard, Heavy or GPU, which validate eagerly and panic on an illegal request.

func GPU

func GPU(r Res) Compute

GPU sizes a task for ECS on an allowlisted EC2 GPU instance. Fargate has no GPU support, so this is a materially different runner. The instance type determines the task's allocatable vCPU and memory (g5g.xlarge currently resolves to 4 vCPU and 7168 MiB after host overhead).

func Heavy

func Heavy(r Res) Compute

Heavy sizes a task for Fargate, for work that does not fit Lambda's 15-minute ceiling. VCPU and MemoryMB must form a legal Fargate pair.

func Standard

func Standard(r Res) Compute

Standard sizes a task for Lambda — the default class. Memory only; Lambda derives vCPU from it.

type Config

type Config = core.Config

Config holds the module identity. Passed to Init() or New().

type Contribution

type Contribution = contributions.Contribution

Contribution is a stored contribution row: OwnerModuleID is the canonical UUID of the contributing module, Payload is its JSON contribution, and ID is retained as a compatibility alias for OwnerModuleID.

func StoredContributions

func StoredContributions(ctx context.Context, slot string) ([]Contribution, error)

StoredContributions returns the contributions other modules have registered into this module's slot (the host-read side of Provide), newest first. The platform's install-time auto-register writes here; a host consumes them at runtime (e.g. oauth-core listing registered auth providers). See core.StoredContributions.

type ContributionSlot

type ContributionSlot = contributions.SlotInfo

ContributionSlot is the manifest projection of a declared slot.

func Contributions

func Contributions() []ContributionSlot

Contributions returns every contribution slot declared on the default module. Useful for tests and for hosts that want to expose a /platform/* read endpoint listing what they accept.

type DeliveryTicket

type DeliveryTicket = core.DeliveryTicket

DeliveryTicket is one minted delivery credential and the CDN origin it is valid against, covering every object under its Prefix. Get one from ms.Delivery and call its URL method per object.

func Delivery

func Delivery(ctx context.Context, prefix string, ttl time.Duration) (DeliveryTicket, error)

Delivery mints ONE delivery credential covering every object under prefix, relative to this module's storage prefix. Scope it to what the entitlement decision actually covered — for video that is one transcode job, i.e. one video across all its renditions — never to bare "_gated/", which authorises everything private the module owns.

ttl is a proposal; read ExpiresIn on the returned ticket for the lifetime actually granted, and propose no more than the entitlement's own remaining life. Fails closed: an error yields a zero ticket, which cannot produce a URL.

type Dependency

type Dependency = core.Dependency

Dependency is the read handle DependencyDB returns — the consumer side of ExposeTable at runtime. It builds STRUCTURED reads only (never raw SQL, never a pool): Select(table).Columns(...).Where(...).WhereIn(...).Limit(n).

func DependencyDB

func DependencyDB(ctx context.Context, producerRef string) *Dependency

DependencyDB returns a read-only handle on a producer module's exposed tables within the current app — the runtime counterpart of ms.DependsOn(..., n.Table(...)). producerRef takes the same forms as DependsOn specs: "@owner/slug", bare "slug", the m<hex> module ID, or a dashed UUID (a trailing @<constraint> is ignored — reads target the version the producer actually runs). The app scope is read from ctx via the SDK's auth identity.

rows, err := ms.DependencyDB(ctx, "@owner/oauth-core").
    Select("users").
    Columns("id", "email").
    WhereIn("id", 1, 2, 3).
    Rows(ctx)

The read executes on the PLATFORM as this module's own per-app DB role in a READ ONLY transaction, authorized against the consent+exposure catalog. There is no cross-plane SQL JOIN — fetch the exposed rows here, read your own tables via mod.DB, and join in application code.

Under `mirrorstack dev --tunnel` a producer running in the SAME dev session is read directly from your local Postgres instead of over the platform proxy — otherwise the proxy would hand you the producer's PRODUCTION rows while your own rows are local, which is wrong data rather than slow data. Two things differ from production on that local path, and both are logged once per producer when it happens: app-owner consent is NOT enforced (see ErrNotExposed), and there is no version anchoring — adding an ms.ExposeTable takes effect on the producer's next boot, whereas production requires a publish AND the app moving to that version. A co-located read also requires an explicit Columns(...) projection; the empty-projection form is resolved by the proxy only.

Dev-plane (`mirrorstack dev --tunnel`) only today; a deployed module reads a co-located producer directly via mod.DB (GRANT SELECT). Panics before Init — matching Platform/Public/Internal.

type DependencyQuery

type DependencyQuery = core.DependencyQuery

DependencyQuery is the structured read builder started by Dependency.Select. Builder methods validate eagerly and latch the first error; Rows/Result surface it without touching the network.

type DependencyResult

type DependencyResult = core.DependencyResult

DependencyResult is one executed dependency read: decoded rows (never nil) plus whether the read was cut at the limit (more rows exist).

type ErrorResponse

type ErrorResponse = httputil.ErrorResponse

ErrorResponse is the SDK's JSON error envelope: {"error": "<message>"}. It is the shape WriteError emits and the same shape the SDK's own auth/permission middleware returns, so module errors and platform errors are indistinguishable on the wire. Exported so callers can decode an error body into a typed value.

type Label

type Label = core.Label

Label is a deferred-resolution display string built from Text (literal) or T (i18n catalog key). Pass it as PermissionOpts.Label / .Description.

func T

func T(key string) Label

T returns a catalog-key Label, resolved per loaded locale at manifest build from catalogs loaded via RegisterMessages.

func Text

func Text(s string) Label

Text returns a literal Label resolving to the default locale.

type MCPToolOption

type MCPToolOption = core.MCPToolOption

MCPToolOption configures an MCPTool declaration (e.g. ToolPermission).

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 rather than metadata.

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 not a warning either, so a destructive tool that stays silent is treated like any other.

ms.MCPTool("delete-user", "Delete a user", deleteUser,
    ms.ToolDestructive(), ms.ToolPermission("users.write"))

func ToolDigest

func ToolDigest[Out any](fn func(Out) any) MCPToolOption

ToolDigest declares how this tool's result compresses, for replay on later turns.

🔴 THE MODULE HAS TO SAY THIS BECAUSE THE PLATFORM CANNOT GUESS IT. Tool results are replayed 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. You know 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 your module knows that.

It runs only when the full result is large enough to be worth shrinking, and never fails the call: a digest that errors is dropped and the platform falls back to truncation.

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 an MCP tool on a module permission (SHORT name, slug-qualified exactly like RegisterPermission). The platform lists and invokes the tool only for callers whose effective permissions include it. An undeclared name registers lazily as admin-only (fail closed).

ms.RegisterPermission("users.read", ms.PermissionOpts{DefaultRole: roles.Viewer()})
ms.MCPTool("list-users", "List users", listUsers, ms.ToolPermission("users.read"))

func ToolReadOnly

func ToolReadOnly() MCPToolOption

ToolReadOnly declares that the tool modifies nothing.

🔴 UNSET IS NOT READ-ONLY. A tool that declares nothing is UNKNOWN, and every reader must treat unknown as "may write". The default could not be readOnly:true — that would silently vouch for every tool ever written, including the ones that delete things.

ms.MCPTool("list-users", "List users", listUsers, ms.ToolReadOnly())

func ToolTitle

func ToolTitle(title string) MCPToolOption

ToolTitle sets a short human-readable display name shown by clients that render a tool list for a person. The wire name stays the identifier.

ms.MCPTool("list-users", "List users", listUsers, ms.ToolTitle("List users"))

type MemberAssertion

type MemberAssertion = core.MemberAssertion

MemberAssertion is one short-lived, platform-signed statement that a named member is acting. Attach Token to a module request as `Authorization: Bearer <token>` and dispatch turns it back into ms.UserID(ctx) for the module that serves it.

Token is a bearer credential — anyone holding it acts as that member until it expires — so keep it server-side. Renew off ExpiresIn, which is the lifetime the platform actually granted, not the ttl you proposed.

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 every other module can trust.

Only the module hosting the `auth-provider` slot may call it: the platform checks the caller against a fact recorded at install, so any other module gets an error whatever it sends. You own WHO the member is; the platform only notarises it, which is what lets other modules read ms.UserID(ctx) without ever holding a credential that can act as that member.

🔴 No app role is in the claims. Identity is not authorization: an assertion says who, never what they may do, and every consuming module still asks its own policy question.

🔴 An error means NO assertion. It never degrades to an anonymous or empty token — an empty token on a request is indistinguishable from no token, so the degraded return would show a signed-in member the signed-out answer while every audit row for their actions names no actor.

ttl is a proposal the platform clamps (5 minutes today); zero takes the platform default.

NOT AVAILABLE YET: the mint endpoint ships with #518 PR 1. Until a platform carrying it is deployed, every call returns an error.

type MetricOption

type MetricOption = meter.MetricOption

MetricOption configures a metric at declaration. The KIND is itself an option (Counter / Gauge), alongside BySubject, Unit, and Price.

func MetricLabel

func MetricLabel(l Label) MetricOption

MetricLabel sets a metric's per-locale display label, built from ms.Text (literal) or ms.T (i18n catalog key), resolved against the module's i18n catalogs at manifest build (mirrors PermissionOpts.Label). Optional.

func MetricUnitLabel

func MetricUnitLabel(l Label) MetricOption

MetricUnitLabel sets a metric's per-locale display label for its UNIT, built from ms.Text (literal) or ms.T (i18n catalog key), resolved against the module's i18n catalogs at manifest build (mirrors MetricLabel). The Unit itself stays the untranslated billing identifier; this is a separate display-only label. Optional.

func Price

func Price(microDollars int64) MetricOption

Price sets a metric's per-unit CUSTOMER price in micro-dollars (1e-6 USD). Optional — omit to meter without charging. The platform charges quantity × this price with NO blanket markup for a module's custom metric.

func Unit

func Unit(u string) MetricOption

Unit sets a metric's display unit (e.g. "order", "byte"). Optional.

type Module

type Module = core.Module

Module is the core SDK instance. Methods on *Module live in internal/core; the type alias makes them callable through this package.

func DefaultModule

func DefaultModule() *Module

DefaultModule returns the default Module created by Init, or nil if Init has not been called.

func New

func New(cfg Config) (*Module, error)

New constructs a Module without setting the package default. Useful in tests or multi-module programs.

type ModuleUI

type ModuleUI = core.ModuleUI

ModuleUI is the module's declared UI surface. Pass to ms.RegisterUI.

type Need

type Need = core.Need

Need is the configuration handle passed to DependsOn / OptionalDependOn callbacks. Use n.Table(name) to declare a SELECT request, n.Event(name) to declare an event subscription.

type Notification

type Notification = core.Notification

Notification is the shape passed to Notify: an i18n Title (required) and Body (optional) — Labels built from ms.Text / ms.T, resolved to per-locale maps at send time so the platform picks the recipient's locale — plus an optional Icon and in-app Link, and the target Audience (defaults to NotifyAdmins when unset).

type NotifyAudience

type NotifyAudience = core.NotifyAudience

NotifyAudience selects WHO inside the app receives a notification: NotifyAdmins (the default) or NotifyAllMembers.

type Observation

type Observation = meter.Observation

Observation is the v2 meter event evidence passed to RecordObservation.

type OnEventOption

type OnEventOption = core.OnEventOption

OnEventOption configures an OnEvent registration.

func OptionalDependOn

func OptionalDependOn(spec string, configure ...func(*Need)) OnEventOption

OptionalDependOn declares an OPTIONAL dependency and returns an OnEventOption to pass into ms.OnEvent. The dep is scoped to the handler — if the dep isn't installed, the event never fires.

type PermanentTaskError

type PermanentTaskError = core.PermanentTaskError

PermanentTaskError marks a task failure as non-retryable while preserving Unwrap.

type PermissionOpts

type PermissionOpts = core.PermissionOpts

PermissionOpts configures a permission declared via RegisterPermission: its default role, extra custom roles, and optional i18n Label/Description.

type Res

type Res = core.Res

Res is the resource request passed to Standard, Heavy or GPU. Each class honours a different subset of its fields and rejects the rest — see core.Res.

type TaskHandler

type TaskHandler = core.TaskHandler

TaskHandler is invoked by the task worker for a registered background task.

type TaskJobStatus

type TaskJobStatus = core.TaskJobStatus

TaskJobStatus is the current state of a local-dev or managed task job.

func TaskStatus

func TaskStatus(ctx context.Context, jobID string) (TaskJobStatus, error)

TaskStatus returns a task's current state from the local dev executor or managed platform.

type TaskOption

type TaskOption = core.TaskOption

TaskOption configures an OnTask registration.

func WithCompute

func WithCompute(c Compute) TaskOption

WithCompute selects a task's execution class and size. Omit it to stay on Standard/Lambda — the default never moves.

func WithEphemeralStorage

func WithEphemeralStorage(sizeBytes int64) TaskOption

WithEphemeralStorage declares Heavy/Fargate runner-local scratch.

func WithMaxRetries

func WithMaxRetries(n int) TaskOption

WithMaxRetries sets the platform-managed retry count.

func WithTimeout

func WithTimeout(d time.Duration) TaskOption

WithTimeout sets the maximum duration for a task handler.

type UIComponent

type UIComponent = core.UIComponent

UIComponent declares one agent-visible React component shipped by the module's web bundle. See ms.ModuleUI for the full shape.

type UIPage

type UIPage = core.UIPage

UIPage is one entry in DefaultPages — a module-shipped React page mounted by the platform under /apps/<app-slug>/<module-slug>/<route> (or /apps/<app-slug>/settings/module/<module-slug>/<route> when Surface is UISurfaceSettings).

type UIProp

type UIProp = core.UIProp

UIProp is one prop declared on a UIComponent. Type is one of "text", "secret", "textarea", "bool", "number", "text-list".

Directories

Path Synopsis
Package audit records what a module did, into the platform's audit store.
Package audit records what a module did, into the platform's audit store.
Package auth provides authentication context and middleware for MirrorStack modules.
Package auth provides authentication context and middleware for MirrorStack modules.
Package cache provides scoped Redis caching with per-app key prefix isolation.
Package cache provides scoped Redis caching with per-app key prefix isolation.
Package db provides multi-tenant PostgreSQL access with app-scoped credential isolation and row-level security.
Package db provides multi-tenant PostgreSQL access with app-scoped credential isolation and row-level security.
Package httpx is the module SDK's one shape for a paginated list response, and the one way to read a page request.
Package httpx is the module SDK's one shape for a paginated list response, and the one way to read a page request.
Package i18n is the module-side internationalization loader for the SDK.
Package i18n is the module-side internationalization loader for the SDK.
Package ids validates and normalizes identifiers carried across module boundaries.
Package ids validates and normalizes identifiers carried across module boundaries.
internal
actor
Package actor carries the dispatch-issued actor delegation between trusted SDK ingestion and outbound inter-module calls.
Package actor carries the dispatch-issued actor delegation between trusted SDK ingestion and outbound inter-module calls.
auditoutbox
Package auditoutbox owns the SDK-private lease and fence mechanics for the public audit package's per-app table.
Package auditoutbox owns the SDK-private lease and fence mechanics for the public audit package's per-app table.
auditstate
Package auditstate links audit.Record to the enclosing ms.Tx without adding a public callback, context setter, or SQL-string inspection seam.
Package auditstate links audit.Record to the enclosing ms.Tx without adding a public callback, context setter, or SQL-string inspection seam.
contributions
Package contributions implements the SDK's host-side contribution slots: a host module declares an extension point with ms.Provide, the SDK auto-mounts HTTP endpoints for register/unregister/list, and other modules register payloads against the slot via the Contribute call (or direct HTTP).
Package contributions implements the SDK's host-side contribution slots: a host module declares an extension point with ms.Provide, the SDK auto-mounts HTTP endpoints for register/unregister/list, and other modules register payloads against the slot via the Contribute call (or direct HTTP).
core
Package mirrorstack is the Go SDK for building modules on MirrorStack.
Package mirrorstack is the Go SDK for building modules on MirrorStack.
ids
Package ids provides identifier helpers shared across SDK packages.
Package ids provides identifier helpers shared across SDK packages.
invocationstate
Package invocationstate owns the private context key shared by the SDK's authenticated transport consumers and the public read-only invocation API.
Package invocationstate owns the private context key shared by the SDK's authenticated transport consumers and the public read-only invocation API.
invocationwire
Package invocationwire implements the private transport representation of the public, read-only invocation context.
Package invocationwire implements the private transport representation of the public, read-only invocation context.
lambdaenv
Package lambdaenv reports whether the process is running inside AWS Lambda.
Package lambdaenv reports whether the process is running inside AWS Lambda.
migration
Package migration reads the module's sql/ directory and applies up/down migrations as a stateless executor.
Package migration reads the module's sql/ directory and applies up/down migrations as a stateless executor.
pgerr
Package pgerr classifies the Postgres error codes the SDK's own infrastructure DDL has to interpret rather than propagate.
Package pgerr classifies the Postgres error codes the SDK's own infrastructure DDL has to interpret rather than propagate.
refcache
Package refcache implements a generic refcount-pinned LRU cache for expensive credential-keyed resources (DB pools, Redis clients).
Package refcache implements a generic refcount-pinned LRU cache for expensive credential-keyed resources (DB pools, Redis clients).
registry
Package registry collects per-module declarations (routes, events, schedules, permissions) for the manifest endpoint to expose to the platform on deploy.
Package registry collects per-module declarations (routes, events, schedules, permissions) for the manifest endpoint to expose to the platform on deploy.
taskenv
Package taskenv reads the canonical one-shot task environment.
Package taskenv reads the canonical one-shot task environment.
Package invocation exposes the platform-authored context for one module request.
Package invocation exposes the platform-authored context for one module request.
Package meter is the module usage-metering surface.
Package meter is the module usage-metering surface.
Package roles defines typed role values passed to ms.RequirePermission.
Package roles defines typed role values passed to ms.RequirePermission.
Package sdktest contains test-only helpers for module contract tests.
Package sdktest contains test-only helpers for module contract tests.
Package storage provides S3 file storage with per-app prefix isolation and CDN URL generation.
Package storage provides S3 file storage with per-app prefix isolation and CDN URL generation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL