Seed & portability
The seed package loads a declarative authorization model — a single JSON or
YAML document — into a model.Storage, and exports one back out. It is both the
human-authored on-ramp behind the aperture check / aperture serve demo and the
full round-trip state file the model portability endpoints use.
One document, both directions
A Document is a flat list of each entity kind. The field tags cover both YAML
and JSON, so either format decodes into the same shape:
accounts: [{ id: acme, name: Acme }]
memberships: [{ principal: alice, account: acme }]
object_types: [{ name: document, actions: [read, write] }]
permissions: [{ id: doc.read, object_type: document, action: read, scope_strategy: implicit }]
principals: [{ id: alice, kind: user, roles: [reader] }]
roles: [{ id: reader, name: Reader, permissions: [doc.read] }]
groups: [ ... ]
grants: [{ id: g1, account: acme, subject: { kind: principal, id: alice }, permission: doc.read, object: "account:acme/document:*", effect: allow }]
templates: [ ... ]
rules: [ ... ]
providers: [ ... ] # runtime wiring, not model state (see below)
Every field mirrors its model counterpart in declarative form. The Document
started as a minimal seed shape and was generalized to the complete model, so an
export file is a strict superset of a seed file: a seed that omits
templates/rules/providers loads unchanged, and a full export reloads through
the very same path. The field set is additive-only, so old seeds keep loading.
Rule ASTs are carried as raw JSON — exactly the rules package’s canonical
Node serialization — so the file never invents a second rule format; it is the
same shape the node editor reads and writes.
Loading (import)
// From bytes, explicit format:
err := seed.Load(ctx, store, data, seed.FormatYAML)
// From a file — format inferred from the extension (.json ⇒ JSON, else YAML):
err := seed.LoadFile(ctx, store, "model.yaml")
Parse decodes the document; Apply upserts it into the store in dependency
order: accounts, object types, permissions, principals, memberships, roles,
groups, grants, templates, then rules. Each write goes through the storage layer’s
own validation — a malformed entity surfaces the same coded error a programmatic
Put would (e.g. APERTURE_ACTION_UNDECLARED for a permission naming an
undeclared action). Rule ASTs are additionally validated against the rules
engine’s contract before storing, so an import rejects a structurally broken rule
(APERTURE_RULE_INVALID) rather than persisting one the engine could never
compile.
Applyis not transactional — a failure may leave a partial model. This is acceptable for the seed-and-demo use case. (The mutation API’s bulk endpoints useStorage.Atomicwhen all-or-nothing is required.)
The YAML path routes through JSON internally (yaml → generic → json → Document)
so the raw-JSON rule AST decodes by exactly the same rules the JSON path uses.
The committed example
seed.Example is the embedded org → project → document fixture stamped to
account acme (seed.ExampleAccount). It is what aperture check loads when no
--seed file is supplied, and it backs the end-to-end test.
Exporting
Export(ctx, store) reads the complete model back out into a Document, and
Marshal(doc, format) renders it to on-disk bytes:
doc, _ := seed.Export(ctx, store)
out, _ := seed.Marshal(doc, seed.FormatJSON)
Export captures every source-of-truth entity: accounts, memberships, object types, permissions, principals, roles, groups, grants, templates, and rule ASTs. Two properties make a round-trip trustworthy:
- Byte-stable. Every slice is emitted in a stable order (sorted by id, name, or natural key) and each rule AST is re-serialized to the rules package’s canonical form, so a re-export of an unchanged model is byte-identical and human-diffable.
- Wildcard edges are preserved. Memberships and grants stamped to the wildcard
account
*(the cross-account super-admin reach) are not among the real accounts, soExportqueries*explicitly — omitting it would silently drop a super-admin’s reach on export/import.
What is not in the file
Two things are deliberately excluded from the model state file:
- Live host domain-object metadata — that is the provider
cache: derived, disposable, never source of truth. Because
Exportreads storage back, and a provider produces no model rows, it is never reproduced. - Provider wiring — the
providers:section is runtime wiring, not model state.Applynever writes it to storage; insteadDocument.BuildRegistry(baseDir)turns it into a live*provider.Registry. The seed file is the source of truth for provider wiring, exactly as auth config is. A declared provider names anobject_type, akind(currently onlycsv), apath(resolved relative to the seed file), and optional cachettl/max_size. A malformed entry isAPERTURE_CONFIG_INVALID/APERTURE_PROVIDER_INVALID.
Related
- The RBAC model — the entities the document mirrors.
- Rules engine — the canonical AST a rule’s
astfield carries. - Providers — the registry
providers:wiring builds, and the cache that is never exported. - Storage — the
StoragebackendApplywrites through andExportreads back. - Portability CLI — the command surface over import/export.