effx
Authoring

Persistence and authority

Typed persistence ports, adapter conformance, PersistentModel and recorded authority.

Persistence ports generate typed services and adapter conformance suites. Models and capabilities remain recorded facts in the IR; they do not generate SQL or an authorization runtime.

AreaStatus
PersistentModel, views, focusRecorded as Model and Focus nodes (implemented, packages/compiler/src/extensions/core.ts).
Capability, AuthorizeRecorded as Capability and Focus nodes with AuthorizedBy and Focuses edges (implemented).
Persistence portsOptional @effx/persistence extension: leaf services and conformance suites; application-owned Effect SQL and Drizzle adapter Layers (spec 0022).
SQL projection from tableSpec 0008 is superseded in scope by spec 0022. No SQL, migrations, ORM schema or CRUD repository is generated.
Cedar authorization, leasesDeferred past the MVP kernel (ADR 0007). Optics are a later runtime mechanism, never the identity.

Persistence ports

import { Operation } from "@effx/runtime";
import { Persist } from "@effx/persistence/syntax";

export const FindUser = Operation.query({
  name: "Users.find", input: FindUserInput, success: User,
})
  .errors(UserNotFound)
  .with(Persist.Port({ port: "Users" }))
  .declare();

Register the compiler half in your project config:

import { defineConfig } from "@effx/cli/config";
import { persistenceExtension } from "@effx/persistence/compiler";

export default defineConfig({ extensions: [persistenceExtension] });

A port operation has no local handler and no HTTP, RPC or CLI exposure. The operation name must begin with <port>.; the remainder names the service method. The compiler emits <port>-port.ts and <port>-conformance.ts, using the original Schema references. Method requirements are never; adapter requirements belong to the Layer selected at the composition root. Core packages do not import the optional persistence package (spec 0022 §§2–3, 7; ADR 0004/0008).

Use builder .declare() for declaration-only ports. Decorators contribute the same annotation data, but a decorated local-bodied method remains a handler and diagnoses EFFX3401. Decorator declaration-only ports are a non-goal: TypeScript cannot decorate abstract or declare members (spec 0022 §2 and the dated implementation amendment).

The generated suite checks closed success/error channels, query purity and repeatability, command rollback, and shared transaction rollback/commit. Supply domain scenarios for seed data, expected successes, which inputs raise declared errors, and the commit observer. The harness owns isolation and the common transaction owner; the compiler cannot derive domain expectations (spec 0022 §4).

One physical store belongs to each test. Between arbitrary samples, the harness resets it and the generated assertion requires its snapshot to equal the empty baseline captured before first seeding. Only then are domain fixtures restored; an incomplete reset fails conformance rather than leaking state into later samples (spec 0022 §4 implementation amendment).

G1 checks all failure reasons against the declared schemas; several typed failures are valid, but defects, interruptions and undeclared failures reject conformance. G2 compares Type-side values and failure multisets, preserving duplicate counts without depending on concurrent completion order. Domain error scenarios and the rollback sentinel retain one expected failure (spec 0022 §4 implementation amendment).

examples/persistence provides Effect SQL and patched drizzle-effect adapters on PGlite, the real PostgreSQL engine compiled to WASM. They share the ambient Effect SQL transaction. PGlite is single-connection: scenarios marked requiresConcurrentConnections are skipped, and lock contention/isolation are not certified. Interruption tests record outcomes without promising rollback in advance. effx generates no SQL, migrations, repository, ORM schema or transport transaction wrapper (spec 0022 §§5–6).

PersistentModel

// decorator: the decorated class is the model schema
@PersistentModel({ table: "users", views: { Public: UserPublic, Self: UserSelf }, focus: { email: ["email"] } })
export class User extends Schema.Class<User>("User")({ /* ... */ }) {}

// builder: the schema is the first argument
export const UserModel = Model.persistent(User, { table: "users", focus: { email: ["email"] } });

PersistentModelOptions

FieldTypeOptionalMeaning
tablestringnoTable name. Recorded on the Model node and as the PersistsAs edge qualifier.
viewsReadonly<Record<string, Schema.Top>>yesNamed view schemas of the model (for example Public, Self). Each becomes a ViewOf edge qualified by its name.
focusReadonly<Record<string, Focus | ReadonlyArray<string>>>yesNamed focus paths inside the model. The IR stores each as a string path.

Source: packages/runtime/src/Annotation.ts (PersistentModelOptions); IR in packages/compiler/src/extensions/core.ts (PersistentModelArgs, persistentModel).

Both forms record PersistentModel [{ ...options, schema }], where schema is the decorated class or the Model.persistent first argument. Model.persistent<S extends Schema.Top>(schema: S, options) evaluates to ModelValue<S>: { _tag: "Model", annotations, schema }.

DecoratorBuilder
@PersistentModel({ table, views?, focus? }) on a class CModel.persistent(C, { table, views?, focus? })

Source: packages/runtime/src/decorators.ts, packages/runtime/src/builder.ts.

IR produced (spec 0001, core.ts):

Node or edgeContent
Model node model:<schema export>name, schema, table, views: [{ name, schema }]
Focus node focus:<Model>.<name>root (the model), path
PersistsAs edgemodel to its schema, qualifier is the table
ViewOf edgeview schema to the model, qualifier is the view name

Model identity is the schema's export, so @PersistentModel class User and Model.persistent(User, ...) name the same model model:User (spec 0002).

Focus

A focus is a serializable identity: a model root plus a key path (ADR 0007).

FieldTypeOptional
_tag"Focus"no
rootSchema.Topno
pathReadonlyArray<string>no

Constructor: Focus.key(root: Schema.Top, ...path: ReadonlyArray<string>) returns that value.

Focus.key(User, "email"); // { _tag: "Focus", root: User, path: ["email"] }

Source: packages/runtime/src/Annotation.ts (Focus), packages/runtime/src/authority.ts.

The frontend lowers Focus.key(Root, "a", "b") or a string array to the path ["a", "b"] (spec 0002 §"Argument lowering"). In the IR the focus root is the model, not a live schema.

Capability

A capability is a kind of authority over a model, optionally narrowed to a focus.

Capability value

FieldTypeOptionalMeaning
_tag"Capability"no
namestringnoCapability name; becomes capability:<name>.
resourceSchema.TopnoThe model schema the authority applies to.
focusFocusyesNarrows the authority to a path in the model.

Constructor: Capability.make(name: string, options: { readonly resource: Schema.Top; readonly focus?: FocusValue }). The runtime exports the value type as CapabilityValue. focus is omitted from the result when undefined.

Source: packages/runtime/src/Annotation.ts (Capability), packages/runtime/src/authority.ts.

The same Capability export also holds the access-expression constructors one, any, all and none. Those belong to Http.Access; see Problems and access.

@Authorize

@Authorize(capability) and .authorize(capability) link an operation to a capability. The frontend lowers a Capability.make call to { name, resource: <resource export>, focus?: string[] }. The interpreter records:

RecordedWhen
Capability node capability:<name>, resource = model:<export>always
AuthorizedBy edge, operation to capabilityalways
Focus node focus:<resource>.<path joined by ".">, Focuses edge capability to focuswhen focus is present

Source: packages/compiler/src/extensions/core.ts (AuthorizeArgs, authorize).

ADR 0007: Cedar deferred

ADR 0007 is accepted and deferred past the MVP kernel. The kernel only records capability and focus facts. The following do not exist yet (docs/decisions/0007-cedar-authorizes-effx-issues-leases.md):

  • the Cedar runtime decision (principal, action, resource, context; default deny; forbid overrides permit);
  • the request-scoped Lease and Lease.modify flow;
  • Cedar schema generation from the IR and EFFX41xx policy diagnostics;
  • optics as the runtime mechanism for focus.

For the mono-web migration, the application reuses its own AccessSpec interpreter with fresh authority resolved inside each committing transaction. See Problems and access.

Example

examples/users/src/user.ts
import { PersistentModel } from "@effx/runtime";
import { Schema } from "effect";
import { Email, UserId, UserPublic, UserSelf } from "./schemas.ts";

@PersistentModel({
  table: "users",
  views: { Public: UserPublic, Self: UserSelf },
  focus: { email: ["email"] },
})
export class User extends Schema.Class<User>("User")({
  id: UserId,
  email: Email,
  displayName: Schema.String,
}) {
  static readonly Public = UserPublic;
  static readonly Self = UserSelf;
}
examples/users/src/capabilities.ts
import { Capability, Focus } from "@effx/runtime";
import { User } from "./user.ts";

export const Read = Capability.make("User.Read", { resource: User });

export const ChangeEmail = Capability.make("User.ChangeEmail", {
  resource: User,
  focus: Focus.key(User, "email"),
});

Builder twin of the model (compiler fixture):

packages/frontend-ts/test/fixtures/users/src/user.builder.ts
import { Model } from "@effx/runtime";
import { User } from "./user.ts";

export const UserModel = Model.persistent(User, { table: "users", focus: { email: ["email"] } });

On this page