@akterjs/cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/{akter-program.config-C4cXx4Fr.mjs → akter-program.config-D8yb9xQP.mjs} +1 -1
- package/dist/akter.cli.mjs +1 -1
- package/dist/command/akter-commands.config.mjs +1 -1
- package/dist/command/akter-program.config.mjs +1 -1
- package/dist/docs/docs.config.mjs +1 -1
- package/dist/docs.config-D2amjsje.mjs +190 -0
- package/dist/{docs.config-_21Xi6HU.mjs → docs.config-D3zzLzLR.mjs} +2 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# @akterjs/cli
|
|
2
2
|
|
|
3
|
-
The Akter CLI,
|
|
3
|
+
The Akter CLI. Installed, its command is `akter`; without installing, run it as `npx @akterjs/cli`.
|
|
4
4
|
|
|
5
5
|
## Start
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npx
|
|
9
|
-
npx
|
|
10
|
-
npx
|
|
11
|
-
npx
|
|
8
|
+
npx @akterjs/cli signup --email you@example.com # emails a code; creates your account
|
|
9
|
+
npx @akterjs/cli organizations create --name acme
|
|
10
|
+
npx @akterjs/cli apps create --organization acme --name notes
|
|
11
|
+
npx @akterjs/cli deploy --organization acme --app notes # bundles src/app.layer.ts and deploys it
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
The CLI talks to Akter Cloud at `https://api.akter.dev` unless `--url` or `AKTER_CLOUD_URL` names another. `npx
|
|
14
|
+
The CLI talks to Akter Cloud at `https://api.akter.dev` unless `--url` or `AKTER_CLOUD_URL` names another. `npx @akterjs/cli dev` runs your app on a local folder with no account, and `npx @akterjs/cli docs` prints these docs in the terminal. Agent apps can do the same through the MCP server at `https://mcp.akter.dev/mcp`.
|
|
15
15
|
|
|
16
16
|
Docs: https://docs.akter.dev. Pricing: https://docs.akter.dev/pricing.
|
|
17
17
|
|
|
@@ -36,7 +36,7 @@ The `akter` command, built on `effect/cli`:
|
|
|
36
36
|
- `akter docs [topic]`: prints the pages of `apps/docs/content`, which `generate` embeds, as Markdown; `--json` prints one line of JSON.
|
|
37
37
|
- Prompts: `Confirmation` (`src/terminal/`) asks only when stdin and stdout are terminals (`TerminalCapabilities.canPrompt`); otherwise it exits 4 at once, so a script or an agent never hangs on a question. `Approval` (`src/registry/approval.service.ts`) uses it for a registry command's confirmation, and turns the API's 428 `ConfirmationRequiredError` into an approval: at a terminal it requests the emailed code, asks for it, confirms it into a token and retries the command once with it; without one it exits 4 with reason `ApprovalRequired` and the `akter approvals` steps. The token is never printed.
|
|
38
38
|
- Exit codes (`CliExitError.codes`): 0 done, 1 failed, 2 usage (and HTTP 400 or 422), 3 authentication (not signed in, HTTP 401 or 403), 4 confirmation or approval needed or declined (and HTTP 428), 5 unavailable (unreachable, timeout, HTTP 408, 429 or 5xx), 130 interrupted. With `--json`, a failure the CLI detects prints `CliExitError`'s JSON (`code`, `reason`, `message`) on stdout.
|
|
39
|
-
- One program, two bins: `src/command/akter-program.config.ts` exports `runAkter`, which runs the command on the process's arguments, and `@akterjs/cli/program` exports it. This package's `akter` bin and the unscoped `akter` package's both call it, so
|
|
39
|
+
- One program, two bins: `src/command/akter-program.config.ts` exports `runAkter`, which runs the command on the process's arguments, and `@akterjs/cli/program` exports it. This package's `akter` bin and the unscoped `akter` package's both call it, so it is this CLI (ADR 0009); npm refused to publish the unscoped name, so `npx @akterjs/cli` is the way to run it without installing. `akter --version` prints the package's version, which `generate` writes into `.generated`, so it follows the lockstep release version.
|
|
40
40
|
- Startup: `pnpm build && pnpm measure:startup` times spawn to first output for local commands on Node and Bun (`scripts/measure-startup.cli.ts`); the target is p95 ≤ 300 ms.
|
|
41
41
|
|
|
42
42
|
An operation behind a feature flag (its `x-akter` hint names one, akter-cloud ADR 0030) still runs when named, but is left out of help and unknown-command suggestions, as is a group whose every command is flagged. The baked-in spec cannot tell which flags an organization has; the Cloud API refuses an organization without the flag. This also applies to the joined `actors fork` command: Cloud forks are behind `cloudFork`, default off. A named fork at a loopback URL still works on `akter dev`, which has no flags. The MCP server hides `actors_fork` from everyone until it can read organization flags; it never falls back to the Cloud-forbidden actor RPC.
|
|
@@ -7019,7 +7019,7 @@ const spec = {
|
|
|
7019
7019
|
]
|
|
7020
7020
|
};
|
|
7021
7021
|
// .generated/compiled/package-version.adapter.js
|
|
7022
|
-
const version = "0.1.
|
|
7022
|
+
const version = "0.1.1";
|
|
7023
7023
|
// src/command/akter-program.config.ts
|
|
7024
7024
|
const main = gen(function* main() {
|
|
7025
7025
|
const operations = yield* operationsOf(spec).pipe(orDie);
|
package/dist/akter.cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { a as withSubcommands, i as withDescription, t as make } from "../Comman
|
|
|
2
2
|
import { importApp } from "../archive/archive-cli.config.mjs";
|
|
3
3
|
import { bench, deploy, exportApp } from "../app-cli.config.mjs";
|
|
4
4
|
import { login, logout, signup } from "../credentials/login.config.mjs";
|
|
5
|
-
import { t as docs } from "../docs.config-
|
|
5
|
+
import { t as docs } from "../docs.config-D3zzLzLR.mjs";
|
|
6
6
|
import { localCommands } from "../local/environment-cli.config.mjs";
|
|
7
7
|
import { logCommands } from "../logs/log-follow.config.mjs";
|
|
8
8
|
import { actorCommands } from "../registry/actor-commands.config.mjs";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as runAkter } from "../akter-program.config-
|
|
1
|
+
export { t as runAkter } from "../akter-program.config-D8yb9xQP.mjs";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as docs } from "../docs.config-
|
|
1
|
+
export { t as docs } from "../docs.config-D3zzLzLR.mjs";
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { L as reduce, M as match, T as join, j as map, rt as mapError } from "./Array-B4kuTQeW.mjs";
|
|
2
|
+
import { n as log } from "./Console-BKHUkH6a.mjs";
|
|
3
|
+
import { $ as fromResult, K as forEach, W as fnUntraced } from "./Effect-CKFpqbmw.mjs";
|
|
4
|
+
import { x as match$1 } from "./Option-DkxSnBhp.mjs";
|
|
5
|
+
import { l as padEnd } from "./String-q5dcsVe4.mjs";
|
|
6
|
+
import { n as optional, r as withDescription, t as String } from "./Argument-D81dqizB.mjs";
|
|
7
|
+
import { i as withDescription$2, m as withDescription$1, o as Boolean, p as withDefault, t as make } from "./Command-Dzq9KM-h.mjs";
|
|
8
|
+
import { CliExitError } from "./terminal/cli-exit.errors.mjs";
|
|
9
|
+
import { lookup, titleOf, topicsOf } from "./docs/doc-topics.pure.mjs";
|
|
10
|
+
import { compactJson } from "./registry/operation-output.pure.mjs";
|
|
11
|
+
// .generated/compiled/docs.adapter.js
|
|
12
|
+
const pages = [
|
|
13
|
+
{
|
|
14
|
+
"markdown": "# Access\n\nA contract names the kinds of callers it accepts, and each command or query can narrow that list. A handler reads who is calling with `Akter.principal`, so rules that depend on the actor's state go in the handler.\n\n## Declare who may call\n\n`callers` maps each kind of caller to the schema of its principal, which always has an `id` (`Akter.PrincipalId`). A member's `callers` lists the kinds that may call it. A member without `callers` accepts every kind the contract declares except `actor`, which a member must list by name. A contract without `callers` accepts one kind, `user`, whose principal is `{ id }`.\n\n```ts title=\"room.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport class NotMemberError extends Schema.TaggedError<NotMemberError>()(\"NotMemberError\", {}) {}\n\nexport const Room = Akter.make(\"Room\", {\n key: Schema.String,\n callers: {\n user: Akter.Principal,\n service: Schema.Struct({ id: Akter.PrincipalId, team: Schema.String }),\n },\n state: Schema.Struct({ members: Schema.Array(Schema.String), posts: Schema.Int }),\n initial: () => ({ members: [], posts: 0 }),\n commands: {\n join: Akter.command({ success: Schema.Void, callers: [\"user\"] }),\n post: Akter.command({\n payload: { text: Schema.String },\n success: Schema.Int,\n error: NotMemberError,\n callers: [\"user\"],\n }),\n reset: Akter.command({ success: Schema.Void, callers: [\"service\"] }),\n },\n queries: {\n posts: Akter.query({ success: Schema.Int }),\n },\n})\n```\n\nA caller of a kind the member doesn't list gets `CallerNotAllowedError`, and the handler never runs. Its message names the refused kind and the declaration that admits it, and the refusal is logged with the actor, the member, the kind and the principal's id.\n\n## Admit API keys\n\nOn Akter Cloud the caller's kind is the kind of credential that signed the request: `user` for a signed-in member, `apiKey` for an API key. A contract without `callers` admits only `user`, so a script, an agent or `akter bench` signed in with an API key is refused until the contract admits it:\n\n```ts title=\"doc.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const Doc = Akter.make(\"Doc\", {\n key: Schema.String,\n callers: { apiKey: Akter.Principal, user: Akter.Principal },\n state: Schema.Struct({ title: Schema.String }),\n initial: () => ({ title: \"Untitled\" }),\n commands: {\n rename: Akter.command({ payload: { title: Schema.String }, success: Schema.Void }),\n },\n})\n```\n\nA member that lists its `callers` must list `\"apiKey\"` too. The reference apps an agent or a bench calls, collab-doc and flash-sale, declare it this way.\n\n## Files: declare who, decide in `authorize`\n\nFile calls (`upload`, `list`, `download`, `usage`) run no handler of their own, so an actor says who may make them in two places. The contract declares which caller kinds may make file calls at all, with `files: Akter.fileAccess({ callers })`. An actor without `files` has no file calls: its RPC group has none, and a typed client of it has no `.files`. The implementation then decides each call in `files.authorize`, which gets the actor's state and the call: `access` is `\"read\"` for a download, a listing or the usage, and `\"write\"` for an upload, and `path` is the file, the listing's prefix, or empty for an upload, whose path the command that attaches it names. The rule reads the caller with `Shelf.principal`, the principal the engine verified, never anything in the request, and the call is refused unless the rule succeeds. A refusal reaches the caller as `FileAccessDeniedError`, and a kind the declaration doesn't list gets `CallerNotAllowedError` before the rule runs.\n\n```ts title=\"shelf.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const Shelf = Akter.make(\"Shelf\", {\n callers: { member: Akter.Principal, user: Akter.Principal },\n commands: { share: Akter.command({ payload: { reader: Schema.String } }) },\n files: Akter.fileAccess({ callers: [\"member\", \"user\"] }),\n initial: () => ({ owner: \"ada\", readers: [] }),\n key: Schema.String,\n state: Schema.Struct({ owner: Schema.String, readers: Schema.Array(Schema.String) }),\n})\n```\n\nHere the owner reads and uploads, and the readers it shared the shelf with only read:\n\n```ts title=\"shelf.actor.ts\"\nimport { Effect } from \"effect\"\nimport { FileAccessDeniedError } from \"@akterjs/effect/contract/errors\"\nimport { Shelf } from \"./shelf.contract.ts\"\n\nexport const ShelfLive = Shelf.toLayer({\n commands: {\n share: (shelf, { reader }) => Shelf.update({ readers: [...shelf.readers, reader] }),\n },\n files: {\n authorize: (shelf, { access, path }) =>\n Effect.gen(function* () {\n const me = yield* Shelf.principal\n const reader = access === \"read\" && shelf.readers.includes(me.id)\n if (me.id !== shelf.owner && !reader) {\n return yield* FileAccessDeniedError.make({ access, message: \"only the owner and its readers\", path })\n }\n }),\n },\n})\n```\n\nThe plain SDK declares the same `files: Akter.fileAccess({ callers })`, and its rule is `files: { authorize: (ctx, { access, path }) => boolean }`, reading `ctx.state` and `ctx.principal`: only `true` allows the call.\n\nOn Akter Cloud, an organization member or an API key reaches an actor's files only when the contract lists its kind, and the rule decides for them too.\n\n### Upgrading an app that uses files\n\nBefore `Akter.fileAccess`, every actor had file calls, open to an app's operators (the `user` and `apiKey` kinds). Now an actor without `files` has none, so a client's `.files` call to it fails. To keep an actor's files as they were, add `files: Akter.fileAccess({ callers: [\"user\"] })` to its contract and `files: { authorize: () => Effect.void }` to its `toLayer`. Add `\"apiKey\"` to the list when the contract declares that kind. Then narrow the rule to the actor's state.\n\nAn app deployed before `Akter.fileAccess` existed is treated the same way: its file calls are refused with `InvalidRequestError` (\"<Actor> declares no file calls…\"), which says how to get them back. Declare `files` and its rule as above and deploy again. The files the actors already hold are kept, and the new build reaches them.\n\n## Fork is for operators\n\n`fork` copies an actor's state and files at a new key, and runs no handler, so it admits only an app's operators: the `user` and `apiKey` kinds, when the contract declares them. Any other kind gets `CallerNotAllowedError`. On Akter Cloud the fork RPC is refused for every caller: forking there is an operator action on the Cloud API, through `akter actors fork`, the `actors_fork` MCP tool or `POST …/environments/{environment}/actors/{actor}/{key}/forks` (`actorForks.create`). A developer, admin or owner of the organization may fork after confirming it, and so may an API key with the `fork` scope and its creator's approval. See [Forking an actor](../reference/cli.md#forking-an-actor).\n\n## Check the rest in the handler\n\n`Room.principal` is the caller's principal, typed by the contract. Here only members may post:\n\n```ts title=\"room.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Room, NotMemberError } from \"./room.contract.ts\"\n\nexport const RoomLive = Room.toLayer({\n commands: {\n join: (room) =>\n Effect.gen(function* () {\n const me = yield* Room.principal\n yield* Room.update({ members: [...room.members, me.id], posts: room.posts })\n }),\n post: (room) =>\n Effect.gen(function* () {\n const me = yield* Room.principal\n if (!room.members.includes(me.id)) {\n return yield* new NotMemberError()\n }\n const posts = room.posts + 1\n yield* Room.update({ members: room.members, posts })\n return posts\n }),\n reset: () => Room.update({ members: [], posts: 0 }),\n },\n queries: {\n posts: (room) => Effect.succeed(room.posts),\n },\n})\n```\n\n## Try it\n\n`story.actor` takes `as`, the caller to act as. The script hides warnings, so the refusal's log line doesn't print:\n\n```ts title=\"room.story.ts\"\nimport { Effect, References } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\nimport { Story } from \"@akterjs/effect/testing\"\nimport { Room } from \"./room.contract.ts\"\nimport { RoomLive } from \"./room.actor.ts\"\n\nconst user = (id: string) => ({ kind: \"user\" as const, principal: { id: Akter.PrincipalId.make(id) } })\nconst janitorService = { kind: \"service\" as const, principal: { id: Akter.PrincipalId.make(\"janitor\"), team: \"ops\" } }\n\nconst program = Story.run(RoomLive, function* (story) {\n const ada = yield* story.actor(Room, \"lobby\", { as: user(\"ada\") })\n const grace = yield* story.actor(Room, \"lobby\", { as: user(\"grace\") })\n const janitor = yield* story.actor(Room, \"lobby\", { as: janitorService })\n\n yield* ada.join()\n console.log(yield* ada.post({ text: \"hi\" }))\n\n const outsider = yield* Effect.flip(grace.post({ text: \"hello\" }))\n console.log(outsider._tag)\n\n const wrongKind = yield* Effect.flip(janitor.post({ text: \"beep\" }))\n console.log(wrongKind._tag)\n\n yield* janitor.reset()\n console.log(yield* ada.posts())\n})\n\nEffect.runPromise(program.pipe(Effect.provideService(References.MinimumLogLevel, \"Error\")))\n```\n\n```text output\n1\nNotMemberError\nCallerNotAllowedError\n0\n```\n\n## Who is calling\n\n`Akter.serve` and `akter dev` use development authentication. A request names its caller in the `akter-principal` header, as JSON such as `{\"kind\":\"user\",\"principal\":{\"id\":\"ada\"}}`, and a request without it is the user `developer`. Because any request can claim any caller, the server listens only on a loopback address and fails with `ListenerNotLoopbackError` on any other unless you pass `unsafeExposeDevelopmentAuth: true`. It also refuses, with 421, a request whose `Host` isn't `localhost`, a 127.0.0.0/8 address or `[::1]`, so a web page that points its own name at your machine can't call your actors from the browser. It refuses, with 403, a request from a page on another origin, and with 415 any POST that isn't `application/ndjson`, so another site can't send a form or a no-cors `fetch` that runs a command as `developer`. Only a page on `localhost`, a 127.0.0.0/8 address or `[::1]` passes the Origin check, so a page on a `*.localhost` name, a `file://` page (which sends `null`) or an extension gets 403. `unsafeExposeDevelopmentAuth` turns the Origin check off, never the Host or NDJSON ones. When you reach the server through another name, such as a dev proxy's hostname, a tunnel or a devcontainer's published port under a LAN name, list it in `allowedHosts` (`akter dev --allowed-host app.test`). Listing a name tells the server that requests for it are yours, so only list names that you control. On Akter Cloud the gateway authenticates each request and forwards the caller.\n\nNo request can claim the `actor` kind. It belongs to messages one actor sends another, which the engine delivers.\n",
|
|
15
|
+
"topic": "core/access"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"markdown": "# Actors\n\nAn actor is one thing your app keeps state for, such as a board, an account or a sale. Each one has a key, its own folder and its own SQLite file. It runs one command at a time, so a handler never sees a half-applied change from another caller.\n\n## Declare an actor\n\n`Akter.make` is the contract: the key, the state and what callers can ask. It holds only schemas, so hosts, browsers and Workers all import it.\n\n```ts title=\"board.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const BoardId = Schema.String.pipe(Schema.brand(\"BoardId\"))\n\nexport const Board = Akter.make(\"Board\", {\n key: BoardId,\n state: Schema.Struct({ title: Schema.String }),\n create: { title: Schema.String },\n commands: {\n rename: Akter.command({ payload: { title: Schema.String }, success: Schema.Void }),\n },\n queries: {\n title: Akter.query({ success: Schema.String }),\n },\n})\n```\n\nA brand on the key, like `BoardId`, stops a board's key being passed where another actor's key belongs.\n\n## Create it, or start it empty\n\nAn actor starts in one of two ways:\n\n- `initial` gives every key a starting state, so an actor exists as soon as something calls it. The counter in [Getting started](../getting-started.md) works this way.\n- `create` takes typed input instead. The actor exists only after a caller creates it, and any other call before that fails with `ActorNotCreatedError`.\n\nA contract declares one or the other. The client has `create(input)`, which fails with `ActorAlreadyExistsError` when the actor exists, and `ensure(input)`, which creates it only if it doesn't.\n\n## Write the handlers\n\n`toLayer` holds the code, and runs only on hosts. `create` turns the input into the first state:\n\n```ts title=\"board.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Board } from \"./board.contract.ts\"\n\nexport const BoardLive = Board.toLayer({\n create: ({ title }) => Effect.succeed({ title }),\n commands: {\n rename: (_board, { title }) => Board.update({ title }),\n },\n queries: {\n title: (board) => Effect.succeed(board.title),\n },\n})\n```\n\n## Let a query answer a little old\n\nA query is fresh by default: every read answers from the actor's latest commit. A query that can show something a moment old, such as a leaderboard or a stock count on a listing page, can say so with `staleFor`, as in `Akter.query({ staleFor: \"2 seconds\", success: Schema.Int })`. An engine may then answer it from a reading at most that old, which Akter Cloud keeps at its edge, so the read never reaches the actor's host. A caller still reads its own writes: a read never answers older than the version the caller's client has already seen, and such a read goes to the host when the kept answer is older. An engine may always answer fresher, and `akter dev` always does. A query without `staleFor`, or one in a build made before it existed, is always fresh. Declaring it is an additive contract change.\n\n## Try it\n\n`Story.run` runs handlers on the local engine in a new temporary folder, with real SQLite, and removes the folder afterwards. `story.actor` returns a client typed by the contract:\n\n```ts title=\"board.story.ts\"\nimport { Effect } from \"effect\"\nimport { Story } from \"@akterjs/effect/testing\"\nimport { Board, BoardId } from \"./board.contract.ts\"\nimport { BoardLive } from \"./board.actor.ts\"\n\nconst roadmap = BoardId.make(\"roadmap\")\n\nconst program = Story.run(BoardLive, function* (story) {\n const early = yield* story.actor(Board, roadmap)\n const refused = yield* Effect.flip(early.title())\n console.log(refused._tag)\n\n yield* story.create(Board, roadmap, { input: { title: \"Roadmap\" } })\n\n const board = yield* story.actor(Board, roadmap)\n yield* board.rename({ title: \"Roadmap 2027\" })\n console.log(yield* board.title())\n})\n\nEffect.runPromise(program)\n```\n\n```text output\nActorNotCreatedError\nRoadmap 2027\n```\n\n## Where it lives\n\nOn disk an actor is a folder: `.akter` locally, an Archil disk on Akter Cloud. The state, the command journal and any tables your migrations add are in one SQLite file you can open with `sqlite3`.\n",
|
|
19
|
+
"topic": "core/actors"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"markdown": "# Commands\n\nA command changes one actor. It runs in one SQLite transaction, and the caller gets the reply only after that transaction is on disk. Queries read and never write.\n\n## Declare commands and queries\n\n`Akter.command` takes the payload's fields and the `success` and `error` schemas, and `Akter.query` takes the same options:\n\n```ts title=\"account.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport class InsufficientFundsError extends Schema.TaggedError<InsufficientFundsError>()(\"InsufficientFundsError\", {\n balance: Schema.Int,\n}) {}\n\nexport const Entry = Schema.Struct({ id: Schema.String, amount: Schema.Int, at: Schema.Number })\n\nexport const Account = Akter.make(\"Account\", {\n key: Schema.String,\n state: Schema.Struct({ balance: Schema.Int }),\n initial: () => ({ balance: 0 }),\n commands: {\n deposit: Akter.command({ payload: { amount: Schema.Int }, success: Schema.Int }),\n withdraw: Akter.command({ payload: { amount: Schema.Int }, success: Schema.Int, error: InsufficientFundsError }),\n },\n queries: {\n entries: Akter.query({ success: Schema.Array(Entry) }),\n },\n})\n```\n\n## Write the handlers\n\nA handler gets the actor's state and the payload, and returns an Effect. `Account.update` sets the new state. `Akter.sql` is the actor's own database, already inside the command's transaction, and `migrations` creates its tables:\n\n```ts title=\"account.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\nimport { Account, InsufficientFundsError } from \"./account.contract.ts\"\nimport type { Entry } from \"./account.contract.ts\"\n\nconst record = (amount: number) =>\n Effect.gen(function* () {\n const sql = yield* Akter.sql\n const id = yield* Akter.id\n const at = yield* Akter.now\n yield* sql`INSERT INTO entries ${sql.insert({ id, amount, at })}`\n })\n\nexport const AccountLive = Account.toLayer({\n migrations: {\n 1: (sql) => sql`CREATE TABLE entries (id TEXT PRIMARY KEY, amount INTEGER NOT NULL, at INTEGER NOT NULL)`,\n },\n commands: {\n deposit: (account, { amount }) =>\n Effect.gen(function* () {\n yield* record(amount)\n const balance = account.balance + amount\n yield* Account.update({ balance })\n return balance\n }),\n withdraw: (account, { amount }) =>\n Effect.gen(function* () {\n yield* record(-amount)\n if (amount > account.balance) {\n return yield* new InsufficientFundsError({ balance: account.balance })\n }\n const balance = account.balance - amount\n yield* Account.update({ balance })\n return balance\n }),\n },\n queries: {\n entries: () => Effect.flatMap(Akter.sql, (sql) => sql<typeof Entry.Type>`SELECT id, amount, at FROM entries`),\n },\n})\n```\n\n## A failed command changes nothing\n\n`withdraw` writes its entry before it checks the balance. When it fails, the whole transaction rolls back, the entry included:\n\n```ts title=\"account.story.ts\"\nimport { Effect } from \"effect\"\nimport { Story } from \"@akterjs/effect/testing\"\nimport { Account } from \"./account.contract.ts\"\nimport { AccountLive } from \"./account.actor.ts\"\n\nconst program = Story.run(AccountLive, function* (story) {\n const account = yield* story.actor(Account, \"ada\")\n yield* account.deposit({ amount: 100 })\n\n const refused = yield* account\n .withdraw({ amount: 150 })\n .pipe(Effect.catchTag(\"InsufficientFundsError\", (error) => Effect.succeed(`refused at ${error.balance}`)))\n console.log(refused)\n\n const entries = yield* account.entries()\n console.log(entries.map((entry) => entry.amount).join(\", \"))\n})\n\nEffect.runPromise(program)\n```\n\n```text output\nrefused at 100\n100\n```\n\nThe caller gets `InsufficientFundsError` as its own class, typed by the contract. A SQL statement that fails reaches the caller as `HandlerFailedError`. In `akter dev` it carries SQLite's message; in Akter Cloud it carries a generic one, since SQLite's names the app's tables and columns, and the full message goes to the host's log under the command's id.\n\n## Stay deterministic\n\nRead ids and the clock through `Akter.id` and `Akter.now`. The journal records the values a command got, so `Verify.replay` gives it the same ones and reaches the same result.\n\nSee [History and replay](history-and-replay.md) for reading that journal and checking recorded outcomes against your handlers.\n\n## No I/O in a command\n\nA command's environment holds only the actor's services, so a network call inside one doesn't compile. Do I/O in a [job](jobs.md), which a command starts and which runs after the command is stored.\n\n## Retries run once\n\nThe clients give each command an id and retry a lost reply with the same id. The actor journals that id with the command, so a retry returns the first result instead of running again.\n",
|
|
23
|
+
"topic": "core/commands"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"markdown": "# History and replay\n\nEvery actor keeps a journal of the commands it ran: their ids, payloads, principals, times and outcomes. Queries aren't journaled. A command whose handler fails is journaled too, but a request refused before the handler runs isn't.\n\nThis is a record of commands, not event sourcing. The actor opens its SQLite database to get its state; it doesn't rebuild state from history.\n\n## Read the journal\n\nThe contract types each entry's payload and outcome:\n\n```ts title=\"counter.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport class NegativeAmountError extends Schema.TaggedError<NegativeAmountError>()(\"NegativeAmountError\", {}) {}\n\nexport const Counter = Akter.make(\"Counter\", {\n key: Schema.String,\n state: Schema.Struct({ count: Schema.Int }),\n initial: () => ({ count: 0 }),\n commands: {\n add: Akter.command({ payload: { by: Schema.Int }, success: Schema.Int, error: NegativeAmountError }),\n },\n})\n```\n\nThis handler writes an audit row with an id and time from Akter. Those values are recorded with the command, so replay can give the handler the same ones:\n\n```ts title=\"counter.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\nimport { Counter, NegativeAmountError } from \"./counter.contract.ts\"\n\nexport const CounterLive = Counter.toLayer({\n migrations: {\n 1: (sql) => sql`CREATE TABLE additions (id TEXT PRIMARY KEY, at INTEGER NOT NULL, amount INTEGER NOT NULL)`,\n },\n commands: {\n add: (counter, { by }) =>\n Effect.gen(function* () {\n if (by < 0) {\n return yield* new NegativeAmountError()\n }\n const sql = yield* Akter.sql\n const id = yield* Akter.id\n const at = yield* Akter.now\n yield* sql`INSERT INTO additions ${sql.insert({ id, at, amount: by })}`\n const count = counter.count + by\n yield* Counter.update({ count })\n return count\n }),\n },\n})\n```\n\n`Counter.history(key)` returns `{ entries, coveredSince }`. Each entry has a `kind`: `recorded` for a command, `created` for creation, `result` for a job result, or `unreadable` when today's contract can't decode an old entry. Narrow by `kind` before using its typed payload and outcome.\n\nThis script serves the counter on the local engine, runs three commands and reads their journal:\n\n```ts title=\"history.ts\"\nimport { Effect, Layer, Option } from \"effect\"\nimport { HttpServer } from \"effect/http\"\nimport { Akter, Verify } from \"@akterjs/effect\"\nimport { akterClient } from \"@akterjs/client/effect\"\nimport { Counter } from \"./counter.contract.ts\"\nimport { CounterLive } from \"./counter.actor.ts\"\n\nconst local = Akter.local(\".akter\").pipe(Layer.provide(CounterLive))\nconst server = Akter.serve({ actors: [Counter], port: 0 }).pipe(Layer.provideMerge(local))\nconst client = Layer.unwrap(HttpServer.addressFormattedWith((url) => Effect.succeed(akterClient({ url }))))\n\nconst program = Effect.gen(function* () {\n const counter = yield* Counter.client(\"clicks\")\n yield* counter.add({ by: 2 })\n yield* counter.add({ by: 3 })\n yield* Effect.flip(counter.add({ by: -1 }))\n\n const history = yield* Counter.history(\"clicks\")\n for (const entry of history.entries) {\n if (entry.kind === \"recorded\") {\n console.log(`${entry.command} ${entry.payload.by} ${entry.exit._tag}`)\n }\n }\n\n const replay = yield* Verify.replay(CounterLive, { actor: Counter, directory: \".akter\", key: \"clicks\" })\n console.log(`replayed ${replay.window.commands}, matched ${Option.isNone(replay.difference)}`)\n})\n\nEffect.runPromise(program.pipe(Effect.scoped, Effect.provide(client.pipe(Layer.provideMerge(server)))))\n```\n\n```text output\nadd 2 Success\nadd 3 Success\nadd -1 Failure\nreplayed 3, matched true\n```\n\n## Read a window\n\nPass `{ since: \"1 hour\" }` to read recent entries. `Counter.historyStream(key, options)` reads the same history as a finite stream of pages, rather than collecting it all in memory. It isn't a live subscription.\n\n`coveredSince` says how far back the retained journal covers. `Counter.historyCoveredSince(key, options)` reads that boundary without collecting entries. Set retention with `Akter.historyRetention(duration)` when constructing the local engine; removing old entries limits both history and replay.\n\n## Replay a command history\n\n`Verify.replay(handlers, { actor, directory, key })` reruns the stored commands in a new in-memory database using the handlers you pass. It supplies the ids and clock values recorded by `Akter.id` and `Akter.now`, and compares command outcomes with their recorded ones. It doesn't change the original database.\n\nThe report gives the replayed window and the first differing outcome, if any, in `difference`. It also reports a difference in file digests under `files`. A matching report isn't a comparison of every database row, so keep assertions about your domain state in tests too.\n\nIn a story, call `story.replay(Counter, key)` instead of passing the directory yourself. A pruned journal fails replay with `HistoryPrunedError`, because it no longer holds every command needed to start from the beginning.\n\n",
|
|
27
|
+
"topic": "core/history-and-replay"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"markdown": "# Jobs\n\nA job does the I/O a command can't: sending email, calling a model or charging a card. A command starts it, and it runs after the command commits. A job is an Effect [Workflow](https://effect.website/docs/workflow). Each `Activity` records its result in the actor's database, so a job that resumes after a restart doesn't repeat activities that finished.\n\n## Declare a job\n\n`Akter.job` takes the payload's fields and the `success` and `error` schemas of its result. `Order.jobs.charge` is a Workflow named `Order/charge`. `Approval` is a durable deferred the job waits on and the `approve` command fills, as [Approve it](#approve-it) shows.\n\n```ts title=\"order.contract.ts\"\nimport { Schema } from \"effect\"\nimport { DurableDeferred } from \"effect/workflow\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const Approval = DurableDeferred.make(\"approval\", { success: Schema.Boolean })\n\nexport const ChargeId = Schema.String.pipe(Schema.brand(\"ChargeId\"))\n\nexport class CardDeclinedError extends Schema.TaggedError<CardDeclinedError>()(\"CardDeclinedError\", {}) {}\n\nexport const Order = Akter.make(\"Order\", {\n key: Schema.String,\n state: Schema.Struct({ total: Schema.Int, status: Schema.String }),\n initial: () => ({ total: 0, status: \"open\" }),\n commands: {\n approve: Akter.command({ payload: { approved: Schema.Boolean, job: Schema.String }, success: Schema.Void }),\n pay: Akter.command({ payload: { amount: Schema.Int }, success: Schema.String }),\n },\n queries: {\n status: Akter.query({ success: Schema.String }),\n },\n jobs: {\n charge: Akter.job({ payload: { amount: Schema.Int }, success: ChargeId, error: CardDeclinedError }),\n },\n secrets: [\"PAYMENTS_KEY\"],\n})\n```\n\n## Start it and handle the result\n\nA command starts its own actor's job with `Akter.start`. The job is stored in that command's transaction, so a failed command starts nothing. The returned id is `<actor>/<key>/<id>`, and replay gives it the same id.\n\nWhen the job ends, its result comes back to the actor as a command. A handler under `results` gets the actor's state and the job's `Exit`, typed by its success and error schemas:\n\n```ts title=\"order.actor.ts\"\nimport { Effect, Exit } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\nimport { Approval, Order } from \"./order.contract.ts\"\n\nexport const OrderLive = Order.toLayer({\n commands: {\n approve: (_order, { approved, job }) => Akter.signal(Approval, { job, value: approved }),\n pay: (order, { amount }) =>\n Effect.gen(function* () {\n yield* Order.update({ total: order.total + amount, status: \"pending\" })\n return yield* Akter.start(Order.jobs.charge, { amount })\n }),\n },\n queries: {\n status: (order) => Effect.succeed(order.status),\n },\n results: {\n charge: (order, result) =>\n Order.update({ total: order.total, status: Exit.isSuccess(result) ? \"paid\" : \"declined\" }),\n },\n})\n```\n\nA result handler receives the actor's state, the job's result and the job itself, `{ id }`, where `id` is the id `Akter.start` returned, so an actor that starts several jobs of one kind knows which one finished. It can update state, send messages, schedule timers and start jobs. Its changes commit with its journal row and the job's final result. Its command id is `result:<job id>`, journaled as `akter.result.charge`, so a retry doesn't apply the result twice. It runs as the actor itself, without a caller gate.\n\n## Write the job\n\nA job's handler is a layer, built with the workflow's `toLayer`. Put it in a `.job.ts` file.\n\nThis example waits an hour, waits for approval, then calls an activity. The activity uses a local stand-in for a payment provider, so the example charges nothing:\n\n```ts title=\"charge.job.ts\"\nimport { Effect, Schema } from \"effect\"\nimport { Activity, DurableClock, DurableDeferred } from \"effect/workflow\"\nimport { Approval, CardDeclinedError, ChargeId, Order } from \"./order.contract.ts\"\n\nexport const ChargeLive = Order.jobs.charge.toLayer(({ amount }) =>\n Effect.gen(function* () {\n yield* DurableClock.sleep({ name: \"hold\", duration: \"1 hour\" })\n\n const approved = yield* DurableDeferred.await(Approval)\n if (!approved) {\n return yield* new CardDeclinedError()\n }\n\n return yield* Activity.make({\n name: \"charge\",\n success: ChargeId,\n error: CardDeclinedError,\n execute: Effect.succeed(ChargeId.make(`receipt:${amount}`)),\n })\n }),\n)\n\n```\n\nPut I/O, clock reads and random values inside an activity's `execute`. The workflow around it runs again when the job resumes, so it must reach the same activities in the same order. The `workflow-determinism` lint rule checks `.job.ts` files.\n\nAn activity interrupted before its result was recorded runs again. Use the provider's idempotency key when the activity has an external effect, such as a charge.\n\n## Change a job's code while jobs run\n\nA job resumes on the code deployed now, not on the code that started it. Its finished activities answer from the record by name, so renaming or removing an activity is safe. Code that decides which activities run, and in what order, must give a job that was already running the answers it got before the deploy.\n\nTo change that code, name the change as a patch in the job's declaration and branch on the job's `patched`:\n\n```ts title=\"invoice.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const Invoice = Akter.make(\"Invoice\", {\n key: Schema.String,\n state: Schema.Struct({ sent: Schema.Boolean }),\n initial: () => ({ sent: false }),\n commands: {\n send: Akter.command({ payload: { to: Schema.String }, success: Schema.String }),\n },\n jobs: {\n deliver: Akter.job({ payload: { to: Schema.String }, patches: [\"review-first\"] }),\n },\n})\n```\n\n```ts title=\"deliver.job.ts\"\nimport { Effect } from \"effect\"\nimport { Activity, DurableClock } from \"effect/workflow\"\nimport { Invoice } from \"./invoice.contract.ts\"\n\nexport const DeliverLive = Invoice.jobs.deliver.toLayer(({ to }) =>\n Effect.gen(function* () {\n if (yield* Invoice.jobs.deliver.patched(\"review-first\")) {\n yield* DurableClock.sleep({ name: \"review\", duration: \"1 hour\" })\n }\n\n yield* Activity.make({ name: \"deliver\", execute: Effect.log(`delivered to ${to}`) })\n }),\n)\n```\n\n`patched(\"review-first\")` is `true` for a job a command started while the contract declared the patch, and `false` for a job started before, for the job's whole run, across restarts and deploys. `Akter.start` stores the patches with the job's payload, so a build rolled back to code without patches still reads the job. A name the declaration doesn't list is a type error, and a defect if it reaches a run. The payload field name `~akter/patches` is Akter's: a job that declares it is refused when its actor is defined, with `ReservedPayloadFieldError`. The plain SDK declares `patches` on `Akter.job` the same way and asks `steps.patched(name)`.\n\nOnce no job started before the patch is still running, delete the old branch and the `patched` call, then remove the name from `patches`. Until then, keep both paths.\n\n## Read a secret\n\nAn actor lists the secrets its jobs may read in `secrets`, as `Order` lists `PAYMENTS_KEY`. A name is an uppercase letter, then uppercase letters, digits or underscores, the same as an environment variable; `Akter.make` refuses any other with `InvalidSecretNameError`. `Order.secret(name)` reads one as an Effect `Redacted`, so it prints as `<redacted>` until you call `Redacted.value`. Only a name the actor lists compiles:\n\n```ts title=\"payments-key.ts\"\nimport { Effect, Redacted } from \"effect\"\nimport { Order } from \"./order.contract.ts\"\n\n/** The provider's authorization header, for an activity's `execute`. */\nexport const authorization = Effect.map(Order.secret(\"PAYMENTS_KEY\"), (key) => `Bearer ${Redacted.value(key)}`)\n```\n\nRead it inside an activity's `execute`, as with other I/O. A command, query, create or result handler that asks is refused, because a handler's result must not depend on a value outside the actor. A read that can't answer fails with `SecretUnavailableError`, whose `reason` is `undeclared` (the actor doesn't list it), `unset` (no value is set), `unavailable` (the engine delivers no secrets or couldn't open them) or `command` (a handler asked).\n\n`akter dev`, scripts and `Story` provide `Akter.jobs`, which reads each secret from the environment by its own name; Bun loads `.env`. On Akter Cloud, `akter secrets set` stores a value for an environment, and a deploy takes the environment's secrets at that moment, so a changed secret applies from the next deploy. A computer gets a credential the same way, as a `secrets` entry on its spec that names one of the actor's secrets and the host it goes to; the platform adds it to the computer's requests to that host, so the computer never sees it. `ensure` reads the value then, so a secret changed by a deploy reaches the computer at its next `ensure`, and a secret that isn't set fails it with `SecretUnavailableError` before the computer is made.\n\n## Wait for a while\n\n`DurableClock.sleep` records when a wait ends, so it survives a restart. `Effect.sleep` doesn't. `DurableDeferred.await` waits for a signal and records its value, so a resumed job reads it back.\n\nA waiting job keeps nothing running. On Akter Cloud, an actor whose jobs only wait has no work due, so it closes once it is idle and the job leaves memory. When the wait ends, the actor opens again: at the clock's time, or for the command that signals the job. A `DurableClock.sleep` of 60 seconds or less runs in memory as an activity instead, so its actor stays open until the sleep ends.\n\n## Approve it\n\nA person or an agent approves a waiting job through a command. `Akter.signal(Approval, { job, value })` signals one of the command's own actor's jobs, by the id `Akter.start` returned, as the `approve` command above does. The signal is stored in the command's transaction and wakes the job once the command commits, so a command that fails signals nothing and a retried command signals once. A job that has already finished, or was never started, ignores it. To approve another actor's job, send that actor a command that signals it.\n\n`DurableDeferred.succeed` works only outside the engine, where something else provides the WorkflowEngine, such as a script on `Akter.local`. A command handler has no WorkflowEngine, and a signal sent around its transaction would reach the job even if the command rolled back, so commands use `Akter.signal`.\n\n`Akter.local` provides the WorkflowEngine that job handlers, `poll` and signals need. `Story.run` does the same in a temporary folder, with a test clock. `story.advance` moves the clock and fires the work that comes due:\n\n```ts title=\"order.story.ts\"\nimport { Effect, Layer, Option } from \"effect\"\nimport { Story } from \"@akterjs/effect/testing\"\nimport { Order } from \"./order.contract.ts\"\nimport { OrderLive } from \"./order.actor.ts\"\nimport { ChargeLive } from \"./charge.job.ts\"\n\nconst program = Story.run(Layer.mergeAll(OrderLive, ChargeLive), function* (story) {\n const order = yield* story.actor(Order, \"o1\")\n const jobId = yield* order.pay({ amount: 300 })\n\n const waiting = yield* Order.jobs.charge.poll(jobId)\n console.log(Option.map(waiting, (result) => result._tag).pipe(Option.getOrElse(() => \"not started\")))\n\n yield* story.advance(\"1 hour\")\n yield* order.approve({ approved: true, job: jobId })\n yield* story.advance(\"1 second\")\n\n const done = yield* Order.jobs.charge.poll(jobId)\n console.log(Option.map(done, (result) => result._tag).pipe(Option.getOrElse(() => \"not started\")))\n console.log(yield* order.status())\n\n const refused = yield* story.actor(Order, \"o2\")\n const refusedJob = yield* refused.pay({ amount: 400 })\n yield* story.advance(\"1 hour\")\n yield* refused.approve({ approved: false, job: refusedJob })\n yield* story.advance(\"1 second\")\n console.log(yield* refused.status())\n})\n\nEffect.runPromise(program)\n```\n\n```text output\nSuspended\nComplete\npaid\ndeclined\n```\n\n`poll(jobId)` returns `Option.none()` before the job has run, `Suspended` while it waits, and `Complete` with its `Exit` once it ends.\n\n## Recovery\n\nEach job lives in its actor's database and moves with the actor. When an actor opens, jobs without a final result resume: completed activities return their recorded results, and clocks wake at their recorded times. If a host stops before a result handler commits, the job resumes and delivers that result again.\n\nIf the actor's database refuses to store a job's result, for example because its disk is full, the host retries it in rounds. A round retries with backoff: from 100 ms, doubling, up to 30 seconds apart, 10 times (about 80 seconds in all). Meanwhile the job's journal holds an `undelivered` entry with the attempts so far, the last error, when it failed, and `retrying`; the inspector shows it with the job's other entries, and it goes away once the result is stored. When a round is spent, `retrying` is `false`, `rounds` counts the spent rounds and `next` says when the next one starts: 5 minutes later, doubling each round up to an hour. Until then the job stays open without keeping its actor busy, and at `next` the host wakes the actor and retries, whether or not anything calls it. Retries never stop for good. Each spent round is logged as an error with the actor, the key, the job and its id. On Akter Cloud a store past its storage ceiling refuses a result the same way when its handler would grow the actor's database: the result and the handler's changes roll back together, the `undelivered` entry's error names the ceiling, and the result is stored once the store has room again.\n\nA job can't start another job. Its result handler can.\n",
|
|
31
|
+
"topic": "core/jobs"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"markdown": "# Live queries\n\nA client can subscribe to a query. It gets the current answer first, then a new one each time a command changes it. Nothing polls: the host is the only writer of an actor, so it knows which tables each command wrote, and it runs a subscribed query again only when one of those tables is a table the query read.\n\n## Subscribe\n\nEvery query on a client has `subscribe`, which takes the query's payload and returns a Stream. Each update is `{ value, version }`, where `version` counts the actor's commands that the value includes.\n\nThis page uses one actor:\n\n```ts title=\"room.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const Room = Akter.make(\"Room\", {\n key: Schema.String,\n state: Schema.Struct({ title: Schema.String }),\n initial: () => ({ title: \"Untitled\" }),\n commands: {\n post: Akter.command({ payload: { text: Schema.String }, success: Schema.Void }),\n rename: Akter.command({ payload: { title: Schema.String }, success: Schema.Void }),\n },\n queries: {\n recent: Akter.query({ payload: { limit: Schema.Int }, success: Schema.Array(Schema.String) }),\n },\n})\n```\n\n```ts title=\"room.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\nimport { Room } from \"./room.contract.ts\"\n\nexport const RoomLive = Room.toLayer({\n migrations: {\n 1: (sql) => sql`CREATE TABLE messages (seq INTEGER PRIMARY KEY, text TEXT NOT NULL)`,\n },\n commands: {\n post: (_room, { text }) => Effect.flatMap(Akter.sql, (sql) => sql`INSERT INTO messages ${sql.insert({ text })}`),\n rename: (_room, { title }) => Room.update({ title }),\n },\n queries: {\n recent: (_room, { limit }) =>\n Effect.flatMap(Akter.sql, (sql) =>\n Effect.map(\n sql<{ text: string }>`SELECT text FROM messages ORDER BY seq DESC LIMIT ${limit}`,\n (rows) => rows.map((row) => row.text),\n ),\n ),\n },\n})\n```\n\nThis script serves the room on the local engine, subscribes to `recent`, and posts a message after each update. Before the second post it renames the room:\n\n```ts title=\"watch.ts\"\nimport { Effect, Layer, Stream } from \"effect\"\nimport { HttpServer } from \"effect/http\"\nimport { Akter } from \"@akterjs/effect\"\nimport { akterClient } from \"@akterjs/client/effect\"\nimport { Room } from \"./room.contract.ts\"\nimport { RoomLive } from \"./room.actor.ts\"\n\nconst server = Akter.serve({ actors: [Room], port: 0 }).pipe(\n Layer.provide(Akter.local(\".akter\").pipe(Layer.provide(RoomLive))),\n)\n\nconst client = Layer.unwrap(HttpServer.addressFormattedWith((url) => Effect.succeed(akterClient({ url }))))\n\nconst program = Effect.gen(function* () {\n const room = yield* Room.client(\"lobby\")\n\n yield* room.recent.subscribe({ limit: 2 }).pipe(\n Stream.take(3),\n Stream.zipWithIndex,\n Stream.runForEach(([{ value, version }, index]) =>\n Effect.gen(function* () {\n console.log(`version ${version}: [${value.join(\", \")}]`)\n if (index === 0) yield* room.post({ text: \"hi\" })\n if (index === 1) {\n yield* room.rename({ title: \"Lunch\" })\n yield* room.post({ text: \"lunch?\" })\n }\n }),\n ),\n )\n})\n\nEffect.runPromise(program.pipe(Effect.scoped, Effect.provide(client.pipe(Layer.provideMerge(server)))))\n```\n\n```text output\nversion 0: []\nversion 1: [hi]\nversion 3: [lunch?, hi]\n```\n\nThere's no update at version 2. `rename` changed only the room's state, which `recent` doesn't read, so the host didn't run the query again.\n\n## What you can rely on\n\n- Every value comes from committed commands. A subscriber never sees a change that could still roll back.\n- A command that changes the answer always sends an update. A command that leaves it the same sends none, so two updates in a row always differ.\n- `room.state` is a Stream of the actor's state that works the same way: the current state first, then a new value each time a command changes it.\n",
|
|
35
|
+
"topic": "core/live-queries"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"markdown": "# Queries\n\nA query reads one actor and returns a value. It runs on a read-only connection to the actor's SQLite file, so it can't change anything, and it sees only what committed commands wrote.\n\n## Declare a query\n\n`Akter.query` takes the same options as `Akter.command`: the payload's fields, and the `success` and `error` schemas.\n\n```ts title=\"room.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const Message = Schema.Struct({ seq: Schema.Int, author: Schema.String, text: Schema.String })\n\nexport const Room = Akter.make(\"Room\", {\n key: Schema.String,\n state: Schema.Struct({ title: Schema.String }),\n initial: () => ({ title: \"Untitled\" }),\n commands: {\n post: Akter.command({ payload: { author: Schema.String, text: Schema.String }, success: Schema.Void }),\n },\n queries: {\n recent: Akter.query({ payload: { limit: Schema.Int }, success: Schema.Array(Message) }),\n count: Akter.query({ payload: { author: Schema.String }, success: Schema.Int }),\n },\n})\n```\n\n## Keep growing data in a table\n\nAn actor's state is read whole by every command and query, so keep it small. Data that keeps growing, like a room's messages, goes in a table in the actor's own database. A command inserts rows with `Akter.sql`, and a query reads only the rows it asks for:\n\n```ts title=\"room.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\nimport { Room } from \"./room.contract.ts\"\nimport type { Message } from \"./room.contract.ts\"\n\nexport const RoomLive = Room.toLayer({\n migrations: {\n 1: (sql) => sql`CREATE TABLE messages (seq INTEGER PRIMARY KEY, author TEXT NOT NULL, text TEXT NOT NULL)`,\n },\n commands: {\n post: (_room, { author, text }) =>\n Effect.flatMap(Akter.sql, (sql) => sql`INSERT INTO messages ${sql.insert({ author, text })}`),\n },\n queries: {\n recent: (_room, { limit }) =>\n Effect.flatMap(\n Akter.sql,\n (sql) => sql<typeof Message.Type>`SELECT seq, author, text FROM messages ORDER BY seq DESC LIMIT ${limit}`,\n ),\n count: (_room, { author }) =>\n Effect.flatMap(Akter.sql, (sql) =>\n Effect.map(\n sql<{ total: number }>`SELECT count(*) AS total FROM messages WHERE author = ${author}`,\n (rows) => rows[0]?.total ?? 0,\n ),\n ),\n },\n})\n```\n\nThe query's payload is checked against its schema before the handler runs, and the result is checked against `success` before it leaves the host.\n\n## Call it\n\nA query is a method on the actor's client, like a command:\n\n```ts title=\"room.story.ts\"\nimport { Effect } from \"effect\"\nimport { Story } from \"@akterjs/effect/testing\"\nimport { Room } from \"./room.contract.ts\"\nimport { RoomLive } from \"./room.actor.ts\"\n\nconst program = Story.run(RoomLive, function* (story) {\n const room = yield* story.actor(Room, \"lobby\")\n yield* room.post({ author: \"ada\", text: \"hi\" })\n yield* room.post({ author: \"grace\", text: \"hello\" })\n yield* room.post({ author: \"ada\", text: \"lunch?\" })\n\n const recent = yield* room.recent({ limit: 2 })\n console.log(recent.map((message) => `${message.author}: ${message.text}`).join(\"\\n\"))\n console.log(yield* room.count({ author: \"ada\" }))\n})\n\nEffect.runPromise(program)\n```\n\n```text output\nada: lunch?\ngrace: hello\n2\n```\n\nTo get a new value each time the answer changes, subscribe to the query instead. See [Live queries](live-queries.md).\n\n## No query across actors\n\nA query reads one actor. Akter has no lookup across actors, because each actor's data is its own SQLite file.\n\nWhen you need a list of actors, keep it in the actor that owns them. For example, a workspace actor can keep a `rooms` table that its commands update, and a query on the workspace lists its rooms. For queries across many owners, like reports, Postgres is a better home for that data.\n",
|
|
39
|
+
"topic": "core/queries"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"markdown": "# Timers and messages\n\nA command can't call another actor or wait, because it holds the actor's transaction. Instead it writes rows that the engine acts on after the command commits: a timer runs one of the actor's own commands later, and a message runs a command on another actor.\n\n## Schedule a command\n\n`Akter.schedule(delay, command, payload)` runs one of the actor's own commands after `delay`, and returns the timer's id. `Akter.cancel(id)` removes a timer that hasn't fired.\n\n## Send a message\n\n`Akter.send(command, key, payload)` runs a command on another actor. The receiving member must list the `actor` caller, and its contract declares that caller's principal as `Akter.ActorPrincipal`, which names the sender's actor and key.\n\n## Try it\n\nAn invite expires a week after it opens. Accepting it adds the person to a team:\n\n```ts title=\"team.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport class InviteExpiredError extends Schema.TaggedError<InviteExpiredError>()(\"InviteExpiredError\", {}) {}\n\nexport const Team = Akter.make(\"Team\", {\n key: Schema.String,\n callers: { user: Akter.Principal, actor: Akter.ActorPrincipal },\n state: Schema.Struct({ members: Schema.Array(Schema.String) }),\n initial: () => ({ members: [] }),\n commands: {\n addMember: Akter.command({ payload: { member: Schema.String }, success: Schema.Void, callers: [\"actor\"] }),\n },\n queries: {\n members: Akter.query({ success: Schema.Array(Schema.String) }),\n },\n})\n\nexport const Invite = Akter.make(\"Invite\", {\n key: Schema.String,\n state: Schema.Struct({ status: Schema.Literals([\"pending\", \"accepted\", \"expired\"]) }),\n initial: () => ({ status: \"pending\" as const }),\n commands: {\n open: Akter.command({ payload: {}, success: Schema.Void }),\n accept: Akter.command({ payload: {}, success: Schema.Void, error: InviteExpiredError }),\n expire: Akter.command({ payload: {}, success: Schema.Void }),\n },\n})\n```\n\n```ts title=\"team.actor.ts\"\nimport { Effect, Layer } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\nimport { Invite, InviteExpiredError, Team } from \"./team.contract.ts\"\n\nconst TeamLive = Team.toLayer({\n commands: {\n addMember: (team, { member }) => Team.update({ members: [...team.members, member] }),\n },\n queries: {\n members: (team) => Effect.succeed(team.members),\n },\n})\n\nconst InviteLive = Invite.toLayer({\n commands: {\n open: () => Effect.asVoid(Akter.schedule(\"7 days\", Invite.commands.expire, {})),\n accept: (invite) =>\n Effect.gen(function* () {\n if (invite.status === \"expired\") {\n return yield* new InviteExpiredError()\n }\n const person = yield* Akter.actorKey\n yield* Invite.update({ status: \"accepted\" })\n yield* Akter.send(Team.commands.addMember, \"core\", { member: person })\n }),\n expire: (invite) => (invite.status === \"pending\" ? Invite.update({ status: \"expired\" }) : Effect.void),\n },\n})\n\nexport const TeamApp = Layer.mergeAll(TeamLive, InviteLive)\n```\n\n`story.advance` moves Effect's `TestClock` forward and fires the timers that come due. `Story.run` uses the `TestClock` that `@effect/vitest` gives each test, and provides its own anywhere else, so this script runs as it is:\n\n```ts title=\"team.story.ts\"\nimport { Effect } from \"effect\"\nimport { Story } from \"@akterjs/effect/testing\"\nimport { Invite, Team } from \"./team.contract.ts\"\nimport { TeamApp } from \"./team.actor.ts\"\n\nconst program = Story.run(TeamApp, function* (story) {\n const ada = yield* story.actor(Invite, \"ada\")\n const bob = yield* story.actor(Invite, \"bob\")\n const team = yield* story.actor(Team, \"core\")\n\n yield* ada.open({})\n yield* bob.open({})\n yield* story.advance(\"1 day\")\n yield* ada.accept({})\n\n yield* story.advance(\"7 days\")\n const late = yield* Effect.flip(bob.accept({}))\n console.log(late._tag)\n console.log((yield* team.members()).join(\", \"))\n})\n\nEffect.runPromise(program)\n```\n\n```text output\nInviteExpiredError\nada\n```\n\n## What you can rely on\n\n- Timers and messages are rows written in the command's transaction. A command that fails leaves neither behind.\n- A timer fires once, at or after its time. Claiming the timer and running its command happen in one transaction.\n- A message is delivered after the sending command commits, and retried until the receiver answers. Each message carries its own command id, so a retry returns the first result instead of running the command twice.\n- A message the receiver refuses, for example with `CallerNotAllowedError` or `ActorNotCreatedError`, isn't retried. The sender's outbox records the refusal and logs a warning.\n- A message doesn't return the receiver's answer to the sender. To call another actor and use the reply, use a [job](jobs.md).\n",
|
|
43
|
+
"topic": "core/timers-and-messages"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"markdown": "# AWS\n\nThere isn't a supported Akter self-hosting recipe for ECS, Fargate or Lambda.\n\nUse [Deploying](overview.md) for the implemented Cloud CLI and its current provisioning limitation. The CLI doesn't accept an AWS region, bucket or access key, and it doesn't create an ECS service.\n\n## The server\n\nThe public SDK provides a local SQLite engine for development. Running copies of that engine behind an AWS load balancer doesn't provide the production engine's placement, ownership fencing or host lifecycle. See [Operating locally](../guides/operating.md) for the scope of the local inspector.\n\n## Storage\n\nThe adopted Cloud design opens each actor's encrypted SQLite database and WAL directly on an Archil mount, under the shard's exclusive checkout. SQLite owns recovery and checkpointing. This page does not provide an AWS host adapter implementing that ownership and durable-commit path. A future guide needs an implemented hosting API and a real deployment test before it can offer infrastructure code.\n",
|
|
47
|
+
"topic": "deploy/aws"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"markdown": "# Cloudflare\n\nAkter contracts and clients can run in Workers, but a Worker isn't the production SQLite host. There isn't a supported Akter 1.0 recipe here for hosting the engine in Cloudflare Containers.\n\nThe old Container/R2 recipe depended on the retired 0.3 server and storage APIs and has been removed. Adding Node compatibility or an R2 binding does not turn that recipe into the current production engine.\n\n## Current deploy path\n\nUse [Deploying](overview.md) for the implemented Cloud CLI and what Akter Cloud doesn't do yet. It uploads through the Cloud API with an Akter API key; you don't supply a Cloudflare token or R2 credentials.\n\n## The server\n\nCloud hosting manages the edge and the stateful host lifecycle. This page doesn't provide a Worker entry, container health endpoint or customer-owned production host. Use [local development](../getting-started.md) for a checked app entry and [clients](../guides/client.md) for calling its actors.\n",
|
|
51
|
+
"topic": "deploy/cloudflare"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"markdown": "# Your own server\n\nThere isn't a supported Akter 1.0 production Docker recipe in this SDK. The old Docker/Caddy/R2 example imported the retired 0.3 server and storage APIs; it has been removed rather than presented as working infrastructure.\n\n## The server\n\n`Akter.local` and `akter dev` run the local development engine on a folder. Packaging them into a container doesn't add the production engine's placement, ownership fencing, authenticated host channels or lifecycle management. Don't scale several development processes over one actor folder.\n\nThe development RPC listener and read-only inspector bind to loopback. Don't expose the inspector through a reverse proxy as an operator endpoint; see [Operating locally](../guides/operating.md#operator-access).\n\n## Current deploy path\n\nUse [Deploying](overview.md) for the implemented Cloud CLI and its current provisioning limitation. You export an app entry, not the old `src/server.ts`, and authenticate with an Akter API key.\n\nA future self-hosting guide needs an implemented production hosting API and a real container deployment/teardown test. There is no checked Dockerfile, Caddy configuration or MinIO storage adapter to copy from this page today.\n",
|
|
55
|
+
"topic": "deploy/docker"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"markdown": "# Fly.io\n\nThere isn't a supported Akter 1.0 hosting recipe for Fly Machines. The old Machine/R2 deployment used the retired 0.3 server and storage APIs; its infrastructure code has been removed.\n\n## Current deploy path\n\nUse [Deploying](overview.md) for the implemented Cloud CLI and its current provisioning limitation. It doesn't create a Fly app, manage Machine replacement or accept a Fly token.\n\n## The server\n\nThe local SQLite engine is a development path, not a production host adapter for a Fly Machine. A future guide must implement and test host ownership, fencing and lifecycle behavior; setting a Machine to stay running isn't that integration. Keep the [local inspector](../guides/operating.md#operator-access) on loopback.\n",
|
|
59
|
+
"topic": "deploy/fly"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"markdown": "# Google Cloud\n\nThere isn't a supported Akter 1.0 deployment recipe for GKE, Cloud Run or Google Compute Engine. The old GKE/R2 recipe imported the retired 0.3 server and storage APIs; it has been removed rather than kept as an unchecked example.\n\n## Current deploy path\n\nUse [Deploying](overview.md) for the implemented Cloud CLI and its current provisioning limitation. The CLI doesn't create a Google Cloud cluster, service account or bucket.\n\n## The server\n\nA Kubernetes pod around the local development engine doesn't provide the production engine's placement, ownership fencing and host lifecycle. The [Kubernetes page](kubernetes.md) explains the same boundary. A future Google Cloud guide needs a supported production host adapter and a real deployment/teardown proof before it can offer cluster code.\n",
|
|
63
|
+
"topic": "deploy/google-cloud"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"markdown": "# Hetzner\n\nThere isn't a supported Akter 1.0 production hosting recipe for a Hetzner server. The old Docker/Caddy/R2 setup depended on the retired 0.3 server and storage APIs; its unchecked infrastructure code has been removed.\n\n## Current deploy path\n\nUse [Deploying](overview.md) for the implemented Cloud CLI and its current provisioning limitation. It doesn't create a Hetzner VM, install Docker or configure a public reverse proxy.\n\n## The server\n\nThe [self-hosting page](docker.md#the-server) distinguishes the local development engine from a production host. Don't expose the development inspector or start competing copies over the same folder. A future Hetzner guide needs an implemented host integration and an executed deployment/teardown test, not only a VM that runs a container.\n",
|
|
67
|
+
"topic": "deploy/hetzner"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"markdown": "# Kubernetes\n\nThere isn't a supported Akter 1.0 production StatefulSet or Helm recipe in this SDK. The old single-pod/R2 example depended on the retired 0.3 server and storage APIs and has been removed.\n\n## The server\n\nA StatefulSet and a persistent volume don't implement the production engine's placement, ownership fencing, authenticated host channels or lifecycle. Running `akter dev` in a pod doesn't supply that integration. The public local engine remains the [development path](../getting-started.md).\n\n## Current deploy path\n\nUse [Deploying](overview.md) for the implemented Cloud CLI and its current provisioning limitation. It doesn't apply manifests or read your kubeconfig.\n\nA future Kubernetes guide needs a supported production host adapter and tests for actual writes, replacement and teardown. This page does not provide unchecked manifests while that API is absent.\n",
|
|
71
|
+
"topic": "deploy/kubernetes"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"markdown": "# Deploying\n\n`akter deploy` bundles an app, uploads it through the Cloud API, confirms that the build is Ready and follows a deployment to `Live`, `NotServing` or `RolledBack`. It uses an Akter API key, not your cloud provider's credentials.\n\n## Sign up and create an app\n\nAkter Cloud's API is at `https://api.akter.dev`, which the CLI uses unless `--url` or `AKTER_CLOUD_URL` names another.\n\n```sh\nnpx @akterjs/cli signup --email you@example.com\nnpx @akterjs/cli organizations create --name acme\nnpx @akterjs/cli apps create --organization acme --name notes\n```\n\n`akter signup` emails you a six-digit code, asks for it, and asks you to accept the [terms of service](../legal/terms.md) and [privacy notice](../legal/privacy.md) once. The session it starts is saved in your OS keychain. An agent without a terminal passes `--code` and `--accept-terms` instead, as [Signing in](../reference/cli.md#signing-in) shows; an agent app can do all of this through the [MCP server](../guides/agents.md) at `https://mcp.akter.dev/mcp` instead.\n\nA new organization runs on the free allowance, $5 of usage a month. `akter billing checkout --organization acme` prints a link to a Stripe page where you add a card; see [Pricing](../pricing.md).\n\n## What a deploy needs\n\nYou need an organization and an app, and a session from `akter login` or an API key with the organization's deploy scope in `AKTER_API_KEY`. A key created with a list of apps reaches only those apps; one without covers every app in the organization. CI and agents use a key: `akter api-keys create` makes one with a name, its scopes (`deploy` for this), a spend cap and an expiry, and prints its secret once.\n\nThe app entry exports `app = { actors, layer }`, the same shape as [local development](../getting-started.md). It must load without starting a listener or running an unbounded program: the CLI builds its layer to inspect contracts and migrations.\n\n## Write the server\n\nYou don't write a separate server entry for the Cloud CLI. Export the actor definitions and their handler layer:\n\n```ts title=\"src/app.layer.ts\"\nimport { Effect, Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect\"\n\nexport const Note = Akter.make(\"Note\", {\n key: Schema.String,\n state: Schema.Struct({ written: Schema.Int }),\n initial: () => ({ written: 0 }),\n commands: {\n write: Akter.command({ payload: { body: Schema.String } }),\n },\n queries: {\n count: Akter.query({ success: Schema.Int }),\n bodies: Akter.query({ success: Schema.Array(Schema.String) }),\n },\n})\n\nexport const app = {\n actors: [Note],\n layer: Note.toLayer({\n migrations: {\n 1: (sql) => sql`CREATE TABLE notes (body TEXT NOT NULL)`,\n 2: (sql) => sql`INSERT INTO notes (body) VALUES (${\"seed\"})`,\n },\n commands: {\n write: (note, { body }) => Effect.gen(function* () {\n const sql = yield* Akter.sql\n yield* sql`INSERT INTO notes (body) VALUES (${body})`\n yield* Note.update({ written: note.written + 1 })\n }),\n },\n queries: {\n count: (note) => Effect.succeed(note.written),\n bodies: () => Effect.gen(function* () {\n const sql = yield* Akter.sql\n const rows = yield* sql<{ body: string }>`SELECT body FROM notes ORDER BY rowid`\n return rows.map((row) => row.body)\n }),\n },\n }),\n}\n```\n\n### Check it\n\nThis small entry demonstrates state and migrations, not a production authorization policy. Define the app's caller kinds and permitted commands using [Access](../core/access.md) before exposing application data.\n\nCheck the entry on real local SQLite before uploading it. Run this script in an empty working folder; it expects the seed row and one write.\n\n```ts title=\"check-app.ts\"\nimport assert from \"node:assert/strict\"\nimport { Effect, Layer } from \"effect\"\nimport { HttpServer } from \"effect/http\"\nimport { Akter } from \"@akterjs/effect\"\nimport { akterClient } from \"@akterjs/client/effect\"\nimport { app, Note } from \"./src/app.layer.ts\"\n\nconst server = Akter.serve({ actors: app.actors, port: 0 }).pipe(\n Layer.provide(Akter.local(\".akter\").pipe(Layer.provide(app.layer))),\n)\nconst client = Layer.unwrap(HttpServer.addressFormattedWith((url) => Effect.succeed(akterClient({ url }))))\n\nconst check = Effect.gen(function* () {\n const note = yield* Note.client(\"lobby\")\n yield* note.write({ body: \"uploaded entry\" })\n assert.equal(yield* note.count(), 1)\n assert.deepEqual(yield* note.bodies(), [\"seed\", \"uploaded entry\"])\n console.log(\"deployment entry checked: state 1; 2 SQL rows\")\n})\n\nEffect.runPromise(check.pipe(Effect.scoped, Effect.provide(client.pipe(Layer.provideMerge(server)))))\n```\n\n```text output\ndeployment entry checked: state 1; 2 SQL rows\n```\n\nThis checks the handlers and numbered SQL migrations. The deploy CLI separately hashes each actor's canonical contract and migrations into its build metadata. It doesn't infer migrations from a database on your laptop.\n\n## Deploy\n\nFrom your app's folder, signed in:\n\n```sh\nnpx @akterjs/cli deploy --organization acme --app notes --entry src/app.layer.ts\n```\n\nIn CI, set `AKTER_API_KEY` through your secret manager instead of signing in. Don't put it in the app entry or command arguments. The organization flag falls back to `AKTER_ORGANIZATION`, then the organization `akter login` saved. The entry defaults to `src/app.layer.ts`; the environment defaults to `prod`, so select the environment explicitly for a preview. See the [CLI reference](../reference/cli.md#akter-deploy) for the full flag list.\n\n## Releases\n\nThe CLI bundles the entry and derives its actor metadata, requests an upload and sends the bytes to the signed URL without its API bearer token. A matching Ready build can be reused without uploading again. It confirms Ready before creating a deployment, then prints the deployment id and its persisted notices once each, in order.\n\nA successful deployment reaches `Live` once every shard has a serving host, prints `Actors at https://<hostname>` and exits 0. The hostname is the environment's own, such as `notes--prod--acme.akter.run`, and an Akter client calls the app's actors there ([Calling actors](../guides/client.md)); a browser opening it gets a 404 that says so. `akter apps get` lists each environment's under `hostnames`, and `akter deploys get` shows a published deploy's `hostname` and `url`. A failed one reaches `RolledBack` and exits nonzero; inspect the failure instead of treating a completed upload as a successful release. A build that was published but had no serving host for every shard within 15 minutes reaches `NotServing`: the CLI prints the reason (for example, no host is up, or the app failed to load) and exits nonzero. It isn't rolled back, because the published build already serves your hostname; deploy a fix to replace it. Build confirmation has a five-minute deadline and deployment following a thirty-minute deadline. A timeout or interrupted CLI does not cancel the remote deployment.\n\nThe current preview's unsupported-owner refusal happens before it changes the live artifact. This isn't a promise that every later publication failure restores a previously published build.\n\n## Storage\n\nThe adopted Cloud design keeps each actor's encrypted SQLite database and WAL directly on the checked-out Archil mount. SQLite owns recovery and checkpointing, and the host waits for durable commit before publishing replies or side effects. A lease or a SQLite lock does not replace the shard's exclusive Archil checkout.\n\nBuild objects are separate from actor state and belong to the provisioned Cloud environment. This CLI uses an Akter API key, not customer storage credentials.\n\n## Where to deploy\n\nThe Cloud CLI is the implemented deployment transport. This page does not provide a supported 1.0 self-hosting recipe for AWS, Google Cloud, Fly.io, Railway, Hetzner, Kubernetes, Docker or Cloudflare Containers. The platform pages state that boundary without unchecked infrastructure code.\n\n`Akter.local` and `akter dev` are the local development path, not a substitute for the production engine's ownership, fencing and host lifecycle.\n\n## Health and shutdown\n\nThe deploy command is finite. It doesn't start your application listener, configure a platform health check, or drain hosts on shutdown. Keep the local [inspector](../guides/operating.md) on loopback; don't expose it as a production operator endpoint.\n\n## Shards\n\nThe Cloud engine owns placement and shard lifecycle. There is no CLI flag here for shard count or region selection.\n\n## Several servers\n\nDon't scale copies of `akter dev` over the same folder to implement a production cluster. Cloud hosts need coordinated shard ownership, fencing and lifecycle management.\n",
|
|
75
|
+
"topic": "deploy/overview"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"markdown": "# Railway\n\nThere isn't a supported Akter 1.0 production Railway recipe. The old service/volume/R2 setup used the retired 0.3 server and storage APIs; its infrastructure code has been removed rather than left unchecked.\n\n## Current deploy path\n\nUse [Deploying](overview.md) for the implemented Cloud CLI and its current provisioning limitation. It doesn't create a Railway service or volume and doesn't accept Railway credentials.\n\n## The server\n\nA persistent volume and a process kept awake don't supply the production engine's placement, ownership fencing or host lifecycle. The local development engine and [inspector](../guides/operating.md) are not that production integration. A future Railway guide needs an implemented hosting API and a real deployment/teardown test.\n",
|
|
79
|
+
"topic": "deploy/railway"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"markdown": "# Getting started\n\nYou'll write a counter actor, run it on a folder on your machine and call it. An actor is one thing your app keeps state for: a counter, a room, a sale.\n\n## Install\n\n```sh\nbun add @akterjs/effect @akterjs/client effect\nbun add -d @akterjs/cli\n```\n\n## Write the contract\n\nThe contract says what a counter holds and what callers can ask of it. It holds only schemas, so clients import it too.\n\n```ts title=\"src/counter.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const CounterId = Schema.String.pipe(Schema.brand(\"CounterId\"))\n\nexport const Counter = Akter.make(\"Counter\", {\n key: CounterId,\n state: Schema.Struct({ count: Schema.Int }),\n initial: () => ({ count: 0 }),\n commands: {\n add: Akter.command({ payload: { by: Schema.Int }, success: Schema.Int }),\n },\n queries: {\n count: Akter.query({ success: Schema.Int }),\n },\n})\n```\n\n`key` names one counter. `initial` is the state of a counter nobody has called yet, so there's no step to create one.\n\n## Write the handlers\n\n```ts title=\"src/counter.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Counter } from \"./counter.contract.ts\"\n\nexport const CounterLive = Counter.toLayer({\n commands: {\n add: (counter, { by }) =>\n Effect.gen(function* () {\n const count = counter.count + by\n yield* Counter.update({ count })\n return count\n }),\n },\n queries: {\n count: (counter) => Effect.succeed(counter.count),\n },\n})\n```\n\nA command runs in one SQLite transaction. `Counter.update` changes the state, and the caller gets the reply only after the change is on disk. A query reads and never writes.\n\n## Run it\n\n`akter dev` serves the actors your app lists, on a folder named `.akter`:\n\n```ts title=\"src/app.layer.ts\"\nimport { Counter } from \"./counter.contract.ts\"\nimport { CounterLive } from \"./counter.actor.ts\"\n\nexport const app = { actors: [Counter], layer: CounterLive } as const\n```\n\n```sh\nbunx akter dev\n```\n\n`akter dev` restarts when you change a file and keeps the data. Each actor is a folder inside `.akter` with its own SQLite file, which you can open with `sqlite3`.\n\n## Call it\n\nThis script calls the counter in the same process. `Akter.local` runs the app on the `.akter` folder, and `Akter.localClient` gives the script the same client that `akterClient` gives a browser or server: every call goes through the same contract checks and caller checks, but nothing listens on a port.\n\n```ts title=\"src/try.ts\"\nimport { Effect, Layer } from \"effect\"\nimport { Akter } from \"@akterjs/effect\"\nimport { app } from \"./app.layer.ts\"\nimport { Counter, CounterId } from \"./counter.contract.ts\"\n\nconst client = Akter.localClient({ actors: app.actors }).pipe(\n Layer.provide(Akter.local(\".akter\").pipe(Layer.provide(app.layer))),\n)\n\nconst program = Effect.gen(function* () {\n const counter = yield* Counter.client(CounterId.make(\"clicks\"))\n yield* counter.add({ by: 2 })\n yield* counter.add({ by: 3 })\n console.log(yield* counter.count())\n})\n\nEffect.runPromise(program.pipe(Effect.scoped, Effect.provide(client)))\n```\n\n```sh\nbun src/try.ts\n```\n\n```text output\n5\n```\n\nRun it again and it prints `10`, because the counter called `clicks` kept its count in `.akter`. To call a running `akter dev` instead, provide `akterClient({ url: \"http://localhost:4000\" })` from `@akterjs/client/effect` in place of `client`.\n\nEach method on `counter` is typed by the contract, so `counter.add({ by: \"two\" })` doesn't compile.\n\n## Next\n\n- [Actors](core/actors.md): keys, state, and actors you create with input.\n- [Commands](core/commands.md): errors, ids and time, and SQL inside a command.\n- [Jobs](core/jobs.md): sending email, calling APIs, anything with I/O.\n",
|
|
83
|
+
"topic": "getting-started"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"markdown": "# Agents\n\nAn agent can run all of Akter Cloud: sign in, create an organization and an app, deploy it, call its actors, and remove everything again. It does that through Akter's hosted MCP server or through the `akter` CLI. Both are built from one list of operations, so they offer the same operations under the same names, take the same input and give the same answers.\n\n## Connect\n\n**An agent app with a person at it**, such as a chat or coding assistant, connects to the MCP server by its URL, `https://mcp.akter.dev/mcp`. The app opens a sign-in page: type your email, then the six-digit code Akter emails you, then approve. The agent then acts as you, with your roles in each organization, until the session ends or you sign it out (`sessions_revoke`, or `akter sessions revoke`). The session lasts 30 days. The other tools check your roles on every call, so a role change or removal applies to them at once; calls to actors carry your roles as they were at the agent's last token, so there it applies with the next token, within a minute.\n\n**A headless agent**, such as a job in CI, sends an agent key as its bearer token instead: `Authorization: Bearer ak_…`. A person creates the key with the scopes, apps, spend cap and expiry the agent needs (`akter api-keys create`). A key with `deploy` scope and no apps list can create apps in its organization. A key limited to named apps can't, and `apps_list` shows it only those apps. The CLI reads the same key from `AKTER_API_KEY`.\n\n## Tools\n\nEvery Cloud API operation is one tool, named `<group>_<operation>` after the `akter <group> <operation>` command: `organizations_create`, `apps_create`, `builds_create`, `deploys_create`, `deploys_get`, `apps_delete` and so on. A tool takes the command's input as one JSON object, its `--input`, and answers with exactly what the command prints with `--json`: the API's own answer, or its error body when the API refuses.\n\n`actors_list` finds an app's actors: it answers with a page of them by actor name and key, optionally only one name (`actor`) and one environment (`environment`, `prod` by default), and with `next` while there is more. A page holds `limit` actors (500 at most, and by default) unless it is the last, which has no `next`, so call it again with `after` set to `next` until `next` is absent.\n\nCalls to an app's actors are the `actors_call`, `actors_query`, `actors_state`, `actors_watch`, `actors_create` and `actors_ensure` tools, as `akter actors <kind>`. Each takes the app's `url`, as the command takes `--url`: a deployed app answers at `https://<app>--<environment>--<organization>.akter.run`, one label under `akter.run`. `actors_watch` takes `count`, as the command takes `--count`, and answers with that many replies, one JSON value per line.\n\nAn actor's files are the `files_list`, `files_usage`, `files_download` and `files_upload` tools, as `akter files <kind>`, with the same `url`. `files_upload` takes the file's bytes as base64 in `content` and answers with the receipt a command attaches at a path. `files_download` answers with the file's bytes as base64 chunks, one per line.\n\nAn actor's computers are the `computers_list`, `computers_get`, `computers_logs`, `computers_pause`, `computers_resume`, `computers_stop` and `computers_delete` tools, as `akter computers <kind>`, with the same `url`. Each but `computers_list` takes the computer's name as `computer`. `computers_logs` answers with a page of the computer's log, oldest first, each line with its `seq`; pass the last `seq` you saw as `after` to read on. There is no tool to make a computer or run a command on one: only the actor's own jobs do that. `computers_delete` removes the computer and its disk, so it asks the person to confirm, as below.\n\nEach tool carries hints an agent app can show: read-only, destructive, idempotent and open-world (it reaches outside Akter, such as sending an email).\n\n## Confirmations\n\nDeleting an app, deleting a computer and other destructive tools need a person to confirm them, whatever the agent was told. The MCP server asks the person through the agent app (MCP elicitation) before it runs one, and refuses when the app can't ask. For a headless agent's key, Akter emails a code to the person who created the key, and the server asks for that code. An agent that already holds an approval token for the action passes it as the tool's `approval` argument, as the CLI takes `--approval`.\n\nChanging the organization's spend cap, up or down (`billing_setCap`), and cancelling its paid plan (`billing_cancel`) need the same confirmation. They also need the organization's billing right, which only its owners hold and no agent key carries today, so a key asking for either gets 403 before any approval is asked for.\n\n## A first deploy\n\n1. `organizations_create` with a name, unless you already belong to one.\n2. `apps_create` with the organization and the app's name. An agent key needs `deploy` scope and no apps list for this; otherwise a person creates the app.\n3. Bundle the app with `akter deploy`, or upload it yourself: `builds_create` answers with a presigned URL; send the bundle to it with an HTTP `PUT`, then `builds_get` until the build is `Ready`.\n4. `deploys_create` for the build, then `deploys_get` until its state is `Live`.\n5. Call the app's actors with `actors_call` and `actors_query`, with `url` set to `https://<app>--<environment>--<organization>.akter.run`.\n6. Clean up with `apps_delete`, which asks you to confirm.\n\n## When a tool fails\n\nA refusal from the API comes back as a tool error whose text is the API's error body, such as `{\"_tag\":\"NotFoundError\",\"resource\":\"app\"}`. A failure the server finds itself, such as input that doesn't match the tool's schema or an API it can't reach, comes back as `{\"_tag\":\"CliExitError\",\"code\":…,\"reason\":…,\"message\":…}` with the CLI's exit code for it. When the session behind a connection ends, the server answers 401 and the agent app signs in again.\n",
|
|
87
|
+
"topic": "guides/agents"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"markdown": "# Migrations\n\nAn actor's tables and state live in its SQLite file. Numbered SQL migrations change tables when the actor next opens. `Akter.migrateState` upgrades stored state that today's schema can't read.\n\n## Start with a stored actor\n\nThis is the first version of a card. Its command writes both state and a note:\n\n```ts title=\"card-v1.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const OldState = Schema.Struct({ name: Schema.String })\n\nexport const CardV1 = Akter.make(\"Card\", {\n key: Schema.String,\n state: OldState,\n initial: () => ({ name: \"Untitled\" }),\n commands: {\n rename: Akter.command({ payload: { name: Schema.String }, success: Schema.Void }),\n },\n})\n```\n\n```ts title=\"card-v1.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\nimport { CardV1 } from \"./card-v1.contract.ts\"\n\nexport const CardV1Live = CardV1.toLayer({\n migrations: {\n 1: (sql) => sql`CREATE TABLE notes (body TEXT NOT NULL)`,\n },\n commands: {\n rename: (_card, { name }) =>\n Effect.gen(function* () {\n const sql = yield* Akter.sql\n yield* sql`INSERT INTO notes ${sql.insert({ body: name })}`\n yield* CardV1.update({ name })\n }),\n },\n})\n```\n\n## Add the next migration\n\nMigrations start at 1 and have no gaps. Keep applied migrations unchanged and add the next number. Akter hashes the SQL and parameters each migration emits: editing an applied migration fails with `MigrationEditedError`, and removing one or leaving a gap fails with `MigrationMissingError`.\n\nHere version 2 adds a category to old notes. The actor keeps the same name and key, so it opens the existing database:\n\n```ts title=\"card.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const Card = Akter.make(\"Card\", {\n key: Schema.String,\n state: Schema.Struct({ title: Schema.String, archived: Schema.Boolean }),\n initial: () => ({ title: \"Untitled\", archived: false }),\n commands: {},\n queries: {\n summary: Akter.query({ success: Schema.String }),\n },\n})\n```\n\n## Upgrade the state\n\nThe old state has `name`; the new state has `title` and `archived`. Give `Akter.migrateState` the old schema and a function that returns the current state, under `state` in `toLayer`:\n\n```ts title=\"card.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\nimport { OldState } from \"./card-v1.contract.ts\"\nimport { Card } from \"./card.contract.ts\"\n\nexport const CardLive = Card.toLayer({\n migrations: {\n 1: (sql) => sql`CREATE TABLE notes (body TEXT NOT NULL)`,\n 2: (sql) => sql`ALTER TABLE notes ADD COLUMN category TEXT NOT NULL DEFAULT 'general'`,\n },\n state: Akter.migrateState(OldState, (old) => ({ title: old.name, archived: false })),\n commands: {},\n queries: {\n summary: (card) =>\n Effect.flatMap(Akter.sql, (sql) =>\n Effect.map(\n sql<{ category: string }>`SELECT category FROM notes ORDER BY rowid LIMIT 1`,\n (rows) => `${card.title}, archived ${card.archived}, category ${rows[0]?.category ?? \"none\"}`,\n ),\n ),\n },\n})\n```\n\nThe state upgrader runs only when the current schema can't decode the stored state. Its result is checked against the current schema before it is stored. An incompatible state without a working upgrader fails with `StateUnreadableError`.\n\nWrite migrations as deterministic SQL. Akter evaluates them to compute their hashes, so don't depend on reading rows to decide which statements to emit. Application tables must not use the reserved `akter_` prefix.\n\n## Test an upgrade\n\nThis script opens version 1, writes a card, closes that engine and opens version 2 on the same folder. It first plans and then applies the pending migration with `Akter.migrate`:\n\n```ts title=\"upgrade.ts\"\nimport { Effect, Layer, Stream } from \"effect\"\nimport { HttpServer } from \"effect/http\"\nimport { Akter } from \"@akterjs/effect\"\nimport { akterClient } from \"@akterjs/client/effect\"\nimport { CardV1 } from \"./card-v1.contract.ts\"\nimport { CardV1Live } from \"./card-v1.actor.ts\"\nimport { Card } from \"./card.contract.ts\"\nimport { CardLive } from \"./card.actor.ts\"\n\nconst oldServer = Akter.serve({ actors: [CardV1], port: 0 }).pipe(\n Layer.provide(Akter.local(\".akter\").pipe(Layer.provide(CardV1Live))),\n)\nconst newServer = Akter.serve({ actors: [Card], port: 0 }).pipe(\n Layer.provide(Akter.local(\".akter\").pipe(Layer.provide(CardLive))),\n)\nconst client = Layer.unwrap(HttpServer.addressFormattedWith((url) => Effect.succeed(akterClient({ url }))))\n\nconst seed = Effect.gen(function* () {\n const card = yield* CardV1.client(\"roadmap\")\n yield* card.rename({ name: \"Roadmap\" })\n}).pipe(Effect.scoped, Effect.provide(client.pipe(Layer.provideMerge(oldServer))))\n\nconst read = Effect.gen(function* () {\n const card = yield* Card.client(\"roadmap\")\n console.log(yield* card.summary())\n}).pipe(Effect.scoped, Effect.provide(client.pipe(Layer.provideMerge(newServer))))\n\nconst program = Effect.gen(function* () {\n yield* seed\n\n const planned = yield* Akter.migrate({ directory: \".akter\", dryRun: true }).pipe(\n Stream.provide(CardLive),\n Stream.runCollect,\n )\n for (const actor of planned) {\n console.log(`${actor.actor}/${actor.key}: ${actor.from} -> ${actor.to}`)\n }\n\n yield* Akter.migrate({ directory: \".akter\", dryRun: false }).pipe(\n Stream.provide(CardLive),\n Stream.runDrain,\n )\n yield* read\n})\n\nEffect.runPromise(program)\n```\n\n```text output\nCard/roadmap: 1 -> 2\nRoadmap, archived false, category general\n```\n\nOpening the actor would also apply its pending migrations. To apply them eagerly across a local store, stop its writers first, then run `akter migrate --all` with your new app. Use `--dry-run` to inspect the numbered migration plan without changing the store. A dry run doesn't execute the SQL or validate a state upgrade, so test the actual upgrade too.\n\nKeep actor type names stable: changing the name points at a different directory rather than renaming old actors. Before removing a command or job, stop creating timers, messages and jobs that refer to it and let pending work finish.\n",
|
|
91
|
+
"topic": "guides/changing-state"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"markdown": "# Calling actors\n\nAny TypeScript program can call actors: a browser app, another backend, a script. `@akterjs/client` has two clients over the same `/rpc` endpoint. `@akterjs/client/effect` gives Effects and Streams typed by the contract, and `@akterjs/client` gives Promises and callbacks.\n\nThis page uses one actor:\n\n```ts title=\"src/counter.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const Counter = Akter.make(\"Counter\", {\n key: Schema.String,\n state: Schema.Struct({ count: Schema.Int }),\n initial: () => ({ count: 0 }),\n commands: {\n add: Akter.command({ payload: { by: Schema.Int }, success: Schema.Int }),\n },\n queries: {\n whoami: Akter.query({ success: Schema.String }),\n },\n})\n```\n\n```ts title=\"src/counter.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Counter } from \"./counter.contract.ts\"\n\nexport const CounterLive = Counter.toLayer({\n commands: {\n add: (counter, { by }) =>\n Effect.gen(function* () {\n const count = counter.count + by\n yield* Counter.update({ count })\n return count\n }),\n },\n queries: {\n whoami: () => Effect.map(Counter.principal, (me) => me.id),\n },\n})\n```\n\n## The Effect client\n\n`akterClient({ url, headers })` is a layer. `Counter.client(key)` returns a handle with one method per command and query, and it needs a `Scope`, because the handle's streams live as long as the scope:\n\n```ts title=\"src/effect-client.ts\"\nimport { Effect } from \"effect\"\nimport { akterClient } from \"@akterjs/client/effect\"\nimport { Counter } from \"./counter.contract.ts\"\n\nexport const clicks = Effect.gen(function* () {\n const counter = yield* Counter.client(\"clicks\")\n return yield* counter.add({ by: 1 })\n}).pipe(Effect.scoped, Effect.provide(akterClient({ url: \"http://127.0.0.1:4000\" })))\n```\n\nA query also has `subscribe`, a Stream of its answer, and the handle has `state`, a Stream of the actor's state. See [Live queries](../core/live-queries.md).\n\n## Cloud browser subscriptions\n\nThe clients above speak the Effect RPC that `akter dev` serves. Akter Cloud's gateway doesn't speak it yet, so they can't call a deployed app; it will (ADR 0016), and then this separate transport goes away. Until then, Akter Cloud's app gateway exposes a native WebSocket at `/live`. Send the app's JWT through the ordered subprotocols, never in a URL or browser storage. The gateway and actor authorize the subscription before the upgrade succeeds, but no reading is delivered until the browser sends `Ready` after `open`:\n\n```ts title=\"src/cloud-live.ts\"\nexport function watchCounter(appOrigin: string, jwt: string, accept: (value: unknown) => void) {\n const url = new URL(\"/live\", appOrigin)\n url.protocol = \"wss:\"\n url.searchParams.set(\"read\", JSON.stringify({ actor: \"Counter\", key: \"clicks\" }))\n const socket = new WebSocket(url, [\"akter-live\", `akter-auth.${jwt}`])\n\n socket.addEventListener(\"open\", () => socket.send(JSON.stringify({ _tag: \"Ready\" })))\n socket.addEventListener(\"message\", ({ data }) => {\n const message = JSON.parse(data)\n if (message._tag === \"Reading\") {\n accept(message.value)\n socket.send(JSON.stringify({ _tag: \"Ack\", id: message.id }))\n }\n })\n\n return socket\n}\n```\n\nOmitting `query` subscribes to state. For a query, add `query: { name, input }` to the read. Each Reading has a committed `version`; keep the last accepted version and include it as `after` when reconnecting. Send Ready on every new socket. A dropped version produces `Resync` followed by a fresh authorized Reading, so replace the local snapshot rather than assuming all intervening commits were replayed.\n\nInstall handlers before sending Ready and do so within 30 seconds, before the token expires. The server retains one bounded latest reading until activation; duplicate Ready frames do not repeat it. Acknowledge readings after accepting them: an unacknowledged or oversized backlog closes only that peer with a typed `Disconnected` reason. Handle `Resync`, `Disconnected`, `error` and `close` in the application's reconnect policy. To stop an open socket, send `{\"_tag\":\"Unsubscribe\"}` and wait for `Unsubscribed` before treating cancellation as complete; it means the host watch has stopped rerunning. Expiry and owner changes require a new authorized connection.\n\n## The Promise client\n\n`createClient({ url, actors })` returns one function per actor. Each call returns a Promise, and `close()` ends the connection. `headers` runs before every request, so it can send a token that changes. With `akter dev` and `Akter.serve`, the `akter-principal` header names the caller (see [Access](../core/access.md#who-is-calling)).\n\nThis script serves the counter on the local engine and calls it as `ada`:\n\n```ts title=\"src/try.ts\"\nimport { Effect, Layer } from \"effect\"\nimport { HttpServer } from \"effect/http\"\nimport { Akter } from \"@akterjs/effect\"\nimport { createClient } from \"@akterjs/client\"\nimport { Counter } from \"./counter.contract.ts\"\nimport { CounterLive } from \"./counter.actor.ts\"\n\nconst server = Akter.serve({ actors: [Counter], port: 0 }).pipe(\n Layer.provide(Akter.local(\".akter\").pipe(Layer.provide(CounterLive))),\n)\n\nconst ada = JSON.stringify({ kind: \"user\", principal: { id: \"ada\" } })\n\nconst program = Effect.gen(function* () {\n const url = yield* HttpServer.addressFormattedWith((address) => Effect.succeed(address))\n const client = createClient({ url, actors: [Counter], headers: () => ({ \"akter-principal\": ada }) })\n\n yield* Effect.promise(async () => {\n const counter = client.Counter(\"clicks\")\n console.log(await counter.add({ by: 2 }))\n console.log(await counter.whoami())\n await client.close()\n })\n})\n\nEffect.runPromise(program.pipe(Effect.provide(server)))\n```\n\n```text output\n2\nada\n```\n\n## Errors\n\nA call fails with the member's declared error, as its own class, or with one of Akter's errors: `CallerNotAllowedError` when the caller's kind may not call the member, `UnauthenticatedError` when the server can't tell who is calling, `ActorNotCreatedError` for an actor that needs `create` first, and `ActorUnavailableError` when the actor can't be opened. A failure to reach the server is an `RpcClientError`. The Effect client puts all of them in the error channel, typed by the contract, and the Promise client rejects with them.\n\n## Retries\n\nThe client gives each command an id and retries it when the request doesn't reach the server or the reply is lost, up to 5 times, waiting 50 ms and then twice as long each time. The actor journals the id with the command, so a retry returns the first result instead of running the command again. Queries don't retry.\n",
|
|
95
|
+
"topic": "guides/client"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"markdown": "# CRUD and when to use Postgres\n\nFor data with an owner, use one actor per owner: a workspace, a user or a document. Keep that owner's rows in tables inside its actor. Commands create, update and delete rows; queries read them.\n\nAkter doesn't give every row an actor. A workspace's tasks can be ordinary SQLite rows, all changed in the workspace's transactions.\n\n## Put tasks in their workspace\n\nThe workspace key chooses the database. The task id identifies a row inside that database:\n\n```ts title=\"workspace.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const Task = Schema.Struct({ id: Schema.String, title: Schema.String, done: Schema.Boolean })\n\nexport class TaskNotFoundError extends Schema.TaggedError<TaskNotFoundError>()(\"TaskNotFoundError\", {\n id: Schema.String,\n}) {}\n\nexport const Workspace = Akter.make(\"Workspace\", {\n key: Schema.String,\n state: Schema.Struct({}),\n initial: () => ({}),\n commands: {\n add: Akter.command({ payload: { title: Schema.String }, success: Schema.String }),\n complete: Akter.command({ payload: { id: Schema.String }, success: Schema.Void, error: TaskNotFoundError }),\n remove: Akter.command({ payload: { id: Schema.String }, success: Schema.Void }),\n },\n queries: {\n tasks: Akter.query({ success: Schema.Array(Task) }),\n },\n})\n```\n\nThe command's SQL and state changes share one transaction. A failure rolls them all back:\n\n```ts title=\"workspace.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\nimport { TaskNotFoundError, Workspace } from \"./workspace.contract.ts\"\n\nexport const WorkspaceLive = Workspace.toLayer({\n migrations: {\n 1: (sql) => sql`CREATE TABLE tasks (\n id TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n done INTEGER NOT NULL CHECK (done IN (0, 1))\n )`,\n },\n commands: {\n add: (_workspace, { title }) =>\n Effect.gen(function* () {\n const sql = yield* Akter.sql\n const id = yield* Akter.id\n yield* sql`INSERT INTO tasks ${sql.insert({ id, title, done: 0 })}`\n return id\n }),\n complete: (_workspace, { id }) =>\n Effect.gen(function* () {\n const sql = yield* Akter.sql\n const changed = yield* sql<{ id: string }>`UPDATE tasks SET done = 1 WHERE id = ${id} RETURNING id`\n if (changed.length === 0) {\n return yield* new TaskNotFoundError({ id })\n }\n }),\n remove: (_workspace, { id }) =>\n Effect.asVoid(Effect.flatMap(Akter.sql, (sql) => sql`DELETE FROM tasks WHERE id = ${id}`)),\n },\n queries: {\n tasks: () =>\n Effect.flatMap(Akter.sql, (sql) =>\n Effect.map(\n sql<{ id: string; title: string; done: number }>`SELECT id, title, done FROM tasks ORDER BY title, id`,\n (rows) => rows.map((row) => ({ id: row.id, title: row.title, done: row.done === 1 })),\n ),\n ),\n },\n})\n```\n\n`complete` refuses a missing task; `remove` is a no-op when the row is already gone. SQLite stores the boolean as 0 or 1, and the query converts it to the contract's Boolean.\n\n## Try it\n\nRows in `alpha` aren't in `beta`, even though both workspaces use the same contract and handlers:\n\n```ts title=\"workspace.story.ts\"\nimport { Effect } from \"effect\"\nimport { Story } from \"@akterjs/effect/testing\"\nimport { Workspace } from \"./workspace.contract.ts\"\nimport { WorkspaceLive } from \"./workspace.actor.ts\"\n\nconst program = Story.run(WorkspaceLive, function* (story) {\n const alpha = yield* story.actor(Workspace, \"alpha\")\n const beta = yield* story.actor(Workspace, \"beta\")\n\n const docs = yield* alpha.add({ title: \"Ship docs\" })\n const tests = yield* alpha.add({ title: \"Write tests\" })\n yield* alpha.complete({ id: docs })\n\n for (const task of yield* alpha.tasks()) {\n console.log(`${task.title}: ${task.done ? \"done\" : \"open\"}`)\n }\n\n yield* alpha.remove({ id: tests })\n console.log(`alpha: ${(yield* alpha.tasks()).length}, beta: ${(yield* beta.tasks()).length}`)\n})\n\nEffect.runPromise(program)\n```\n\n```text output\nShip docs: done\nWrite tests: open\nalpha: 1, beta: 0\n```\n\nSeparate databases aren't authorization. Check that a caller may use the selected workspace in your handlers; see [Access](../core/access.md). The example leaves that check out to show the data model.\n\n## Shared data\n\nFor shared data with modest writes, one database actor can own it. Its commands are serialized, so it gives shared tables one writer and one transaction boundary. It also puts all of those writes through that actor; use it when that fits the workload, rather than splitting the data only to need a cross-actor transaction later.\n\nA stateless endpoint can be a Worker or an Effect HttpApi handler that calls the relevant actor client. The endpoint doesn't need its own actor.\n\n## Use Postgres when it is simpler\n\nChoose Postgres for:\n\n- Global unique constraints, such as a slug that must be unique across every workspace.\n- Transactions that must change rows belonging to different owners together.\n- Reporting that joins data across many owners.\n- Existing tools or integrations built around Postgres.\n\nAn actor transaction covers one actor, not the whole app. [Messages](../core/timers-and-messages.md) coordinate actors after commit, and jobs can call other services, but neither makes separate databases one atomic transaction.\n",
|
|
99
|
+
"topic": "guides/crud-and-postgres"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"markdown": "# Exporting an app\n\n`akter export` copies every actor of an Akter Cloud app into one `.akter` archive on your machine. Each actor arrives as its own SQLite database, holding its state, its tables and its command history. Import the archive into a folder with `akter import`, then open the databases with `sqlite3`, serve them with `akter dev`, and read each actor's history with `akter history`.\n\nThis guide covers running an export, what it holds, how to resume one and how the CLI checks what it downloads. The flags are listed in the [CLI reference](../reference/cli.md#akter-export).\n\n## Run an export\n\nYou need an API key with export scope for the organization. A key with only deploy, call or read scope is refused. Put it in `AKTER_API_KEY` and name the organization, the app and the archive to write:\n\n```sh\nexport AKTER_API_KEY=\"<export-scoped key>\"\nbunx akter export \\\n --organization \"$AKTER_ORGANIZATION\" \\\n --app chat \\\n --url \"$AKTER_CLOUD_URL\" \\\n --out ./chat.akter\n```\n\nThe key only reaches that organization's app. Every listing, snapshot and download request is authorized for it, and none of them reach another app or organization.\n\nThe CLI goes through the app's actors one at a time. For each one it prints progress every MiB and when the download finishes, then how many actors it exported, and last the archive it wrote:\n\n```text\nRoom/lobby: 1048576 of 2359296 bytes\nRoom/lobby: 2097152 of 2359296 bytes\nRoom/lobby: 2359296 of 2359296 bytes\nRoom/archive: erased, nothing to export\nExported 1 actors to /home/me/chat.akter.staging\nWrote /home/me/chat.akter with 1 entries\n```\n\nAn export is complete only when it prints `Wrote <archive> with <n> entries` and exits 0. Treat any other exit as a failed export and keep the message it printed.\n\n## What an export holds\n\nThe archive is a tar file compressed with zstd. Its first entry is `manifest.json`, which records the format version, when the archive was written, and the size and SHA-256 of every other entry. Its `computers` section lists every computer the app's actors keep: the actor, the computer's name, image, size and egress hosts, and its newest checkpoint when it has one. A computer's disk stays in Akter Cloud, so its checkpoint is recorded with `\"portable\": false` and the archive holds none of its files. Its `lineage` section has one entry per forked actor: the actor, the actor it was forked from (`parent`), the fork's id (`fork`) and the parent's commit version at the cut (`forkedAt`). The entries after it use the layout `akter dev` uses:\n\n```text\nmanifest.json\nshards/<shard>/<Actor>/<key>/actor.db\nshards/<shard>/<Actor>/<key>/files/<sha256>\n```\n\n`tar --zstd -tf chat.akter` lists them, `manifest.json` first.\n\nEach `actor.db` is a plain, decrypted SQLite database. It holds the actor's whole database at one consistent point after its last committed command:\n\n- its state and its own tables,\n- its command journal, which is the history `akter history` reads,\n- its pending timers, outbox and jobs.\n\nEach `files/<sha256>` is one distinct file the actor's database names, its plaintext content named by its SHA-256; the file's paths, size and time stay in `actor.db`. The export holds nothing else: no Cloud keys, no export records and no file keys, since the CLI blanks every file grant in `actor.db` before it archives it.\n\nEach actor is snapshotted on its own, after a commit and while no command runs on it, so each database is consistent. Actors are exported one after another, though, so two actors' snapshots come from different moments. Don't read an export as one snapshot of the whole app.\n\n## Use the exported data\n\nRead the archive into a new folder. The CLI checks every entry against its manifest, takes each actor's files into the folder's own file home, `files-home`, and creates the folder only once every entry has passed and every file is in:\n\n```sh\nbunx akter import ./chat.akter --dir ./chat-export\n```\n\nThen serve the exported actors locally with the same app entry you deploy:\n\n```sh\nbunx akter dev --dir ./chat-export\n```\n\nOr do both in one step: `bunx akter dev --from ./chat.akter --dir ./chat-export`.\n\nRead one actor's history from the folder:\n\n```sh\nbunx akter history Room/lobby --dir ./chat-export\n```\n\nVerify it, from your app's folder: `--verify` replays the actor's commands against the handlers you deploy now and reports `no difference`, or the first command whose outcome differs. Akter Cloud has no verify command of its own; this export, import and replay is how you verify a Cloud app's history.\n\n```sh\nbunx akter history Room/lobby --entry src/app.layer.ts --dir ./chat-export --verify\n```\n\nOr open a database directly with stock `sqlite3`:\n\n```sh\nsqlite3 ./chat-export/shards/<shard>/Room/lobby/actor.db \".tables\"\n```\n\n## Erased actors\n\nAn erased actor has nothing to export. The CLI prints `<Actor>/<key>: erased, nothing to export`, writes nothing for it and goes on. Erased actors aren't counted in the final `Exported <n> actors` line.\n\n## Resume an interrupted export\n\nThe CLI downloads into a staging folder beside the archive, `chat.akter.staging`, and writes the archive only when every actor is in. While an actor downloads, `actor.db.export` holds the bytes so far and `actor.db.export.json` records the snapshot they come from. The file is renamed to `actor.db` only after it passes its check.\n\nIf an export stops partway, run the same command again with the same `--out`:\n\n- An actor that already has an `actor.db` is kept as it is, and the CLI prints `<Actor>/<key>: already exported`.\n- A partly downloaded actor prints `<Actor>/<key>: resuming` and continues from the same snapshot, starting where the kept bytes end.\n- Every other actor is snapshotted and downloaded as usual.\n\nKept actors aren't refreshed. To export the app as it is now, delete the staging folder or use a new `--out`. Once the archive is written, the staging folder is removed, and an `--out` that already exists is refused.\n\nStarting an actor's export replaces that actor's previous snapshot on the server. Run one export of an app at a time, or one run can replace the snapshot another is still downloading.\n\n## How the download is checked\n\nThe snapshot stays encrypted on the server under a key of its own, and only the download decrypts it. When the server takes the snapshot, it also records the database's SHA-256 and seals the snapshot with a MAC over every page in order, which it checks before it reports the snapshot. The MAC covers the snapshot file only: a page rolled back inside the live `actor.db` on the server is outside what export can detect.\n\nThe CLI downloads at most 64 KiB per request and hashes each piece as it writes it. On a resume, it first hashes the bytes it kept, then continues. When the download is complete, it compares the hash with the one the server recorded and renames the file to `actor.db` only if they match. So a finished `actor.db` is byte for byte the database the server snapshotted.\n\nWhen a check fails, the CLI deletes that actor's partial download and exits nonzero:\n\n- The download doesn't match its SHA-256: `Room/lobby did not match its SHA-256. Run akter export again to start that actor over.`\n- The snapshot was replaced, or failed authentication or its page MAC check on the server: `The snapshot of Room/lobby is gone (<reason>). Run akter export again to start that actor over.`\n\nRun the command again. Finished actors are kept, and the failed actor starts over from a new snapshot.\n\n## Known limits\n\n- An imported fork still names the actor it was forked from by Akter Cloud's identity, `<Actor>/<key>`, in its file records, and the folder's file home doesn't record that actor as its parent. Its state and files are served as usual, and the archive's manifest lineage still names its parent. Only the local record of where it came from stays in Cloud's form.\n",
|
|
103
|
+
"topic": "guides/exporting"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"markdown": "# Operating locally\n\nThe local inspector reads the actor databases in your development folder. It shows state, SQL tables, recent commands, timers, outbox entries, failed deliveries, job records and file names. It doesn't run commands or repair the database.\n\n`akter dev` starts it beside the RPC listener, on port 4001 by default. You can also start it with `Akter.inspector({ directory })`.\n\n## Inspect what was stored\n\nThis example writes a note, closes its engine, then reads the persisted state and table through the inspector.\n\n```ts title=\"note.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const Note = Akter.make(\"Note\", {\n key: Schema.String,\n state: Schema.Struct({ written: Schema.Boolean }),\n initial: () => ({ written: false }),\n commands: {\n write: Akter.command({ payload: { title: Schema.String } }),\n },\n})\n```\n\n```ts title=\"note.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\nimport { Note } from \"./note.contract.ts\"\n\nexport const NoteLive = Note.toLayer({\n migrations: {\n 1: (sql) => sql`CREATE TABLE notes (title TEXT NOT NULL)`,\n },\n commands: {\n write: (_note, { title }) =>\n Effect.gen(function* () {\n const sql = yield* Akter.sql\n yield* sql`INSERT INTO notes ${sql.insert({ title })}`\n yield* Note.update({ written: true })\n }),\n },\n})\n```\n\nThe script decodes just the inspection fields it needs. The state is a JSON string; decode it with the contract's state schema. Use an empty folder for this check, since it expects one stored note:\n\n```ts title=\"inspect-note.ts\"\nimport assert from \"node:assert/strict\"\nimport { Effect, Layer, Schema } from \"effect\"\nimport { FetchHttpClient, HttpClient, HttpServer } from \"effect/http\"\nimport { Akter } from \"@akterjs/effect\"\nimport { akterClient } from \"@akterjs/client/effect\"\nimport { Note } from \"./note.contract.ts\"\nimport { NoteLive } from \"./note.actor.ts\"\n\nconst directory = \".akter\"\nconst server = Akter.serve({ actors: [Note], port: 0 }).pipe(\n Layer.provide(Akter.local(directory).pipe(Layer.provide(NoteLive))),\n)\nconst client = Layer.unwrap(HttpServer.addressFormattedWith((url) => Effect.succeed(akterClient({ url }))))\n\nconst write = Effect.gen(function* () {\n const note = yield* Note.client(\"lobby\")\n yield* note.write({ title: \"hello\" })\n}).pipe(Effect.scoped, Effect.provide(client.pipe(Layer.provideMerge(server))))\n\nconst Inspection = Schema.Struct({\n state: Schema.String,\n commands: Schema.Array(Schema.Struct({ command: Schema.String })),\n tables: Schema.Array(Schema.Struct({\n name: Schema.String,\n rows: Schema.Array(Schema.Array(Schema.String)),\n total: Schema.Int,\n })),\n})\n\nconst inspect = Effect.gen(function* () {\n const url = yield* HttpServer.addressFormattedWith((address) => Effect.succeed(address))\n const response = yield* HttpClient.get(`${url}/api/actors/Note/lobby`)\n assert.equal(response.status, 200)\n const inspection = yield* Schema.decodeUnknownEffect(Inspection)(yield* response.json)\n const state = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Note.state))(inspection.state)\n assert.equal(state.written, true)\n const table = inspection.tables.find((table) => table.name === \"notes\")\n assert.deepEqual(table?.rows, [[\"hello\"]])\n assert.equal(table?.total, 1)\n assert.equal(inspection.commands.length, 1)\n console.log(\"state and table persisted; 1 journal entry\")\n}).pipe(\n Effect.scoped,\n Effect.provide(Layer.mergeAll(Akter.inspector({ directory, port: 0 }), FetchHttpClient.layer)),\n)\n\nEffect.runPromise(Effect.gen(function* () {\n yield* write\n yield* inspect\n}))\n```\n\n```text output\nstate and table persisted; 1 journal entry\n```\n\nThe two Effects have separate scopes: inspection happens after the writer's scope closes. The folder stays on disk. The docs runner gives the whole program a fresh temporary working folder each time.\n\n## Inspector routes\n\n| Route | Response |\n| --- | --- |\n| `GET /` | The inspector page. |\n| `GET /api/actors` | Stored actor types and their keys. |\n| `GET /api/actors/:actor/:key` | One actor's inspection as JSON. Encode the type and key as URL path segments. |\n| `GET /api/actors/:actor/:key/live` | Server-sent events, checked every 500 ms and emitted when the inspection changes. |\n\nThe inspector opens SQLite in query-only mode. An actor that isn't stored returns 404; an unreadable actor returns 503. It shows the latest 100 commands and up to 100 rows per developer table, with the table's full row count. It's a diagnostic view, not a full export.\n\n## Operator access\n\nThe inspector binds to `127.0.0.1` and defaults to port 4001. `allowedHosts` can add Host names, but doesn't authenticate a caller. Don't expose or proxy it to public traffic: it can reveal state, payloads and job records.\n\nThe old `OperatorAuth.tokens`, capability grants and `POST /operator` setup aren't part of this local SDK. The inspector is read-only; it isn't that operator endpoint.\n\n## History\n\nUse [History and replay](../core/history-and-replay.md) for typed journal reads, retained-history boundaries and replay. Configure local retention with `Akter.historyRetention` when constructing the engine. Retention can't restore commands already pruned; the inspector's last 100 commands aren't the full history.\n\n## Snapshots, branches and promote\n\nThe local engine snapshots, branches and promotes the same way Akter Cloud does, so you can rehearse a risky change, such as a migration or an import, on a copy and switch to it only when it looks right. Durable Objects don't offer [point-in-time recovery in local development](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/); Akter does.\n\nA data directory holds environments. It starts with one, `dev`, whose store is the folder itself. `akter dev` serves the one promoted to serve.\n\n1. `akter dev snapshots create --name before-import` asks for a snapshot of the serving environment. The running `akter dev` copies each actor's database with SQLite's `VACUUM INTO` while it holds that actor's command lock, so each copy holds every command committed before it and none after, and it copies the environment's files with them. Each database is consistent on its own, which is also what a Cloud snapshot guarantees; two actors' copies can be a moment apart. The snapshot is kept under `.akter/snapshots/<id>`.\n2. `akter dev environments branch --name trial --snapshot before-import` copies the snapshot into a new environment, `.akter/environments/trial`. A branch starts quiet: `akter dev --environment trial` serves its calls and queries, but no timer fires, no outbox message is delivered and no job resumes. They are held, not dropped, because a schedule must survive what happens to its actor.\n3. `akter dev environments promote --name trial` makes `trial` serve. The running `akter dev` switches to it within a second and lifts its quiet: jobs resume from the snapshot's journal, held outbox messages are delivered, and timers that came due while it was quiet fire.\n4. `akter dev environments promote --name dev` promotes back. The environment you leave is kept, quiet and untouched, until you delete it.\n\nThe promote says what it leaves out, and so does this guide:\n\n- Writes made to `dev` after the snapshot are not in `trial`.\n- On a promote back, writes `trial` took while it served are not in `dev`.\n\nA repeated promote changes nothing and prints the same result. Local and Cloud behave the same here: the same names, the same `snapshot-<n>` ids, the same quiet branch, and the same journal that jobs resume from once a branch is promoted. See the [CLI reference](../reference/cli.md#akter-dev-snapshots-and-akter-dev-environments) for every command.\n\nAn `.akter` archive of a data directory is one environment's export: it holds the serving environment's actors, files and catalog, and never `environments.db`, a snapshot or another environment. An archive an earlier CLI packed from a whole data directory, `environments.db` included, still imports and serves the environment its record named.\n\n## When something goes wrong\n\nRead the concrete error before changing a stored folder; see [Errors](../reference/errors.md). Don't delete an actor database or write default state to make an open succeed.\n\nUse [Migrations](changing-state.md) to plan table and state changes, and [Testing](testing.md) to check them on real SQLite. This local guide doesn't promise production host recovery, cloud backups, or the old 0.3 telemetry and operator APIs.\n",
|
|
107
|
+
"topic": "guides/operating"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"markdown": "# Testing\n\n`Story.run` runs your handlers on the local engine, with real SQLite in a fresh temporary folder. It removes the folder when the story ends. Commands, queries, timers, messages and jobs use the same engine as `Akter.local`.\n\nA story is an Effect. You can run it as a script with `Effect.runPromise`, or return it from an `it.effect` test in `@effect/vitest`.\n\n## Test a failure, not only a reply\n\nThis account has a starting balance and an audit table:\n\n```ts title=\"account.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport class InsufficientFundsError extends Schema.TaggedError<InsufficientFundsError>()(\"InsufficientFundsError\", {}) {}\n\nexport const Account = Akter.make(\"Account\", {\n key: Schema.String,\n state: Schema.Struct({ balance: Schema.Int }),\n initial: () => ({ balance: 50 }),\n commands: {\n withdraw: Akter.command({ payload: { amount: Schema.Int }, success: Schema.Int, error: InsufficientFundsError }),\n },\n queries: {\n balance: Akter.query({ success: Schema.Int }),\n auditRows: Akter.query({ success: Schema.Int }),\n },\n})\n```\n\nThe handler writes an audit row before checking the balance. Refusing the withdrawal must roll that row back as well as leaving the balance alone:\n\n```ts title=\"account.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\nimport { Account, InsufficientFundsError } from \"./account.contract.ts\"\n\nexport const AccountLive = Account.toLayer({\n migrations: {\n 1: (sql) => sql`CREATE TABLE audit (amount INTEGER NOT NULL)`,\n },\n commands: {\n withdraw: (account, { amount }) =>\n Effect.gen(function* () {\n const sql = yield* Akter.sql\n yield* sql`INSERT INTO audit ${sql.insert({ amount })}`\n if (amount > account.balance) {\n return yield* new InsufficientFundsError()\n }\n const balance = account.balance - amount\n yield* Account.update({ balance })\n return balance\n }),\n },\n queries: {\n balance: (account) => Effect.succeed(account.balance),\n auditRows: () =>\n Effect.flatMap(Akter.sql, (sql) =>\n Effect.map(sql<{ total: number }>`SELECT count(*) AS total FROM audit`, (rows) => rows[0]?.total ?? 0),\n ),\n },\n})\n```\n\n## Write a story\n\n`story.actor(Contract, key)` returns a client typed by the contract. Read back through queries so the test checks what was stored, not just the command's return value:\n\n```ts title=\"account.story.ts\"\nimport assert from \"node:assert/strict\"\nimport { Effect, Option } from \"effect\"\nimport { Story } from \"@akterjs/effect/testing\"\nimport { Account } from \"./account.contract.ts\"\nimport { AccountLive } from \"./account.actor.ts\"\n\nconst program = Story.run(AccountLive, function* (story) {\n const ada = yield* story.actor(Account, \"ada\")\n const grace = yield* story.actor(Account, \"grace\")\n\n const refused = yield* Effect.flip(ada.withdraw({ amount: 80 }))\n assert.equal(refused._tag, \"InsufficientFundsError\")\n assert.equal(yield* ada.balance(), 50)\n assert.equal(yield* ada.auditRows(), 0)\n\n assert.equal(yield* ada.withdraw({ amount: 30 }), 20)\n assert.equal(yield* ada.balance(), 20)\n assert.equal(yield* ada.auditRows(), 1)\n assert.equal(yield* grace.balance(), 50)\n\n const replay = yield* story.replay(Account, \"ada\")\n assert.equal(replay.window.commands, 2)\n assert.equal(Option.isNone(replay.difference), true)\n console.log(\"rollback, stored state, isolation and replay passed\")\n})\n\nEffect.runPromise(program)\n```\n\n```text output\nrollback, stored state, isolation and replay passed\n```\n\nIf `withdraw` unexpectedly succeeds, `Effect.flip` fails the story. If it changes the balance or leaves its audit row behind, the assertions fail. The successful withdrawal and the other account check that rollback didn't hide a broken success path or mix the two actors' state.\n\n## Drive time and background work\n\nA story's commands drain immediately due messages, timers and jobs before returning. `story.advance(\"1 hour\")` moves the test clock and drains the work that comes due. Don't sleep for real time to guess when a timer or job has finished.\n\n`Story.run` uses a runner's TestClock when one is present and supplies its own otherwise. See [Timers and messages](../core/timers-and-messages.md) and [Jobs](../core/jobs.md) for runnable examples of time and job results.\n\n## Test callers and creation\n\nPass `{ as: { kind, principal } }` to `story.actor` to act as a caller your contract declares. Use `story.create(Contract, key, { input })` for an actor with explicit creation. The [Access](../core/access.md) and [Actors](../core/actors.md) guides exercise those cases.\n\n## What a story doesn't prove\n\nA story uses local SQLite. It doesn't simulate a distributed host failing, remote storage losing a reply, or the network between regions. Test restart behavior by closing an engine and reopening the same folder, as the [Migrations](changing-state.md) guide does, and test storage and host failures in the system that actually runs them.\n\n`story.replay` checks recorded command outcomes with the supplied handlers. Keep state assertions too: replay doesn't compare every database row. See [History and replay](../core/history-and-replay.md).\n",
|
|
111
|
+
"topic": "guides/testing"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"markdown": "# Privacy notice\n\nversion: 2026-11-14\n\nThis notice says what personal data Akter Cloud keeps about you, why, and for how long. Akter Cloud is operated by Rika Labs, LLC, 805 N Roosevelt St, Suite 100, Boise, ID 83706, USA, which is responsible for that data. It shares its version with the [terms of service](./terms.md); accepting one at sign-up accepts both.\n\n## What Akter keeps\n\n- **Your account:** your email address, when the account was created, and which version of these documents you accepted and when.\n- **Sign-in:** one-time codes, kept only until they're used or expire, and your sessions, each with its creation and expiry time. A session's secret is shown to you once, at sign-in; agent keys are stored as hashes.\n- **Organizations:** your memberships and roles, invitations you send or receive, and the agent keys you create, with their scopes, apps, caps and expiry.\n- **Usage:** metered usage per organization, app and key, for limits and billing.\n- **Operational logs and traces:** request metadata such as times, operations and error types, used to run and secure the service.\n\nThe data your apps' actors store belongs to you and is processed only to run your apps; see the terms.\n\n## Who processes it\n\nAkter runs on infrastructure providers: Cloudflare (edge and Workers), Archil (storage and compute), Resend (sign-in email), Axiom (logs and traces) and Stripe (billing). Each receives only what it needs for its part.\n\n## How long\n\n- Codes expire after minutes; sessions after 30 days; logs and traces after the retention stated in the docs.\n- Deleting your account (`akter account delete`) ends its sessions and deletes the account record. Records the law requires Akter to keep, such as invoices, are kept for the required period.\n\n## Your choices\n\nYou can read your account (`akter whoami`), export your apps' data (`akter export`), revoke sessions and keys, and delete your account. Other requests, including access to or deletion of your personal data: dallenpyrah+akter@rikalabs.sh, or write to Rika Labs, LLC at the address above.\n",
|
|
115
|
+
"topic": "legal/privacy"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"markdown": "# Terms of service\n\nversion: 2026-11-14\n\nThese terms cover your use of Akter Cloud: the Cloud API, the `akter` CLI when it talks to Akter Cloud, and the hosted MCP server. Akter Cloud is provided by Rika Labs, LLC, 805 N Roosevelt St, Suite 100, Boise, ID 83706, USA (\"Akter\" in these terms). The open-source packages (`@akterjs/*`) are licensed under Apache-2.0 on their own; these terms don't change that license.\n\n## Your account\n\n- You sign up with an email address and a one-time code. You're responsible for keeping access to that mailbox and for every session, agent key and agent you allow to act for you.\n- An agent you connect, through the CLI, an agent key or the MCP server, acts as you within the roles and limits you gave it. You're responsible for what it does.\n- Akter asks a person to confirm dangerous actions, such as deleting an app. Don't build systems that confirm on a person's behalf.\n\n## Your apps and data\n\n- You keep your rights in the code you deploy and the data your actors store. You give Akter the rights it needs to run, store, back up and export them for you.\n- You can export your data at any time with `akter export`. Deleting an environment or an account deletes its data after the retention period stated in the docs.\n- Don't use Akter Cloud to break the law, to harm others, to attack or overload other systems, or to store data you have no right to store.\n\n## Usage, limits and billing\n\n- Usage is metered and priced as published. Spend caps on your organization and on agent keys stop new spend when they're reached.\n- A new metered dimension, or a higher price for an existing one, is announced at least 30 days before it is charged, with the date it starts, which is the earliest it can start. Only usage on or after that date is charged at the new price; earlier usage never is.\n- A command or query is billed when it's made with your credentials and Akter Cloud did its work for it: an authenticated, authorized call answered on your app's behalf, including your actor's own errors and the refusal of a malformed payload, such as one that doesn't match the call's schema. It isn't billed when it's refused for authentication or authorization (a 401 or 403, including an actor refusing a caller its rules don't allow), stopped by a spend cap, refused by a rate limit or an admission limit (such as a store past its disk ceiling or a session past its watch limit), or when it fails on Akter's side (a 5xx, an actor that's unavailable, or a request that hangs or is cancelled). A retry under the same command id that returns the recorded outcome is billed once.\n- An actor isn't charged for being connected or idle: the hosts that run it are included. Commands and queries are charged per call, stored data per GiB-month, and an actor's computers for the time they run.\n- Payment is handled by Stripe. Card details never pass through Akter, the CLI or an agent.\n\n## Service\n\n- Akter Cloud is provided as is, without warranties beyond those the law requires. Availability targets, if any, are stated separately.\n- Akter may suspend an account that breaks these terms or puts the service or other customers at risk, and will say why where it can.\n- These terms may change. A change gets a new version, and you'll be asked to accept it before your next sign-up or as described in the change.\n\n## Contact\n\nQuestions about these terms: dallenpyrah+akter@rikalabs.sh, or write to Rika Labs, LLC at the address above.\n\nSee also the [privacy notice](./privacy.md).\n",
|
|
119
|
+
"topic": "legal/terms"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"markdown": "# Pricing\n\nAkter Cloud charges for what your apps use, by the unit, with no seats and no tiers. The SDKs, the clients, the CLI and `akter dev` are open source (Apache-2.0) and free to use on your own machine.\n\n## Prices\n\n| What | Price |\n| --------------------------------- | ------------------------------------------------------- |\n| Commands | $1.25 per million |\n| Queries | $1.00 per million |\n| Storage | $0.50 per GiB-month, the first GiB-month free each month |\n| Snapshots | $0.10 per GiB-month |\n| Computer vCPU | $0.14 per vCPU-hour |\n| Computer memory | $0.05 per GiB-hour |\n| Computer exec | $0.13 per 1,000 calls |\n| Egress | $0.18 per GB |\n| Live updates, timers, forks, steps | Free |\n\nAn actor isn't charged for being connected or idle: the hosts that run your actors are included in the per-call prices, and an idle app holds no host at all. A computer is charged only while it runs.\n\n## Plans\n\n- **Free.** Every organization gets $5 of usage a month without a card. When it's used up, new spend stops with a typed error until the next month or until you add a card.\n- **Paid.** Add a card on a page Stripe hosts: `akter billing checkout --organization <organization>` prints the link. A paid organization pays a $5 monthly minimum that its usage counts toward, so a month with $3 of usage is billed $5 and a month with $40 is billed $40. Card details never pass through Akter, the CLI or an agent.\n- **Spend cap.** A paid organization starts with a $50 monthly cap, and every agent key can carry its own. A cap stops new spend when it's reached. Change it with `akter billing set-cap`, which a person confirms.\n- **Failed payments.** After a failed payment you have 7 days to fix the card before new spend stops.\n\n## What is billed\n\nA command or query is billed when it's made with your credentials and Akter Cloud did its work: an authenticated, authorized call answered for your app, including your actor's own errors. Calls refused for authentication or authorization, stopped by a spend cap or a rate limit, or failed on Akter's side are not billed. A retry under the same command id is billed once. The [terms of service](legal/terms.md) give the full rule.\n\n## Watching your spend\n\n- `akter usage get --organization <organization>` reads a month's usage and spend per meter, for the organization, one app or one key. Usage shows up within about two minutes.\n- `akter usage forecast --organization <organization>` projects this month's spend to the month's end.\n- `akter billing invoices --organization <organization>` lists invoices with links to Stripe's pages for them.\n\nAgents read the same numbers through the `usage_get`, `usage_forecast` and `billing_invoices` tools of the [MCP server](guides/agents.md).\n\n## Price changes\n\nA new metered unit, or a higher price for an existing one, is announced at least 30 days before it's charged, with the date it starts. Only usage on or after that date is charged at the new price.\n",
|
|
123
|
+
"topic": "pricing"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"markdown": "# CLI\n\nThe `akter` executable belongs to `@akterjs/cli`. In an app workspace with that package installed, run `bunx akter`. Use `akter --help` and `akter <command> --help` for the flags in your installed build.\n\n| Command | Status |\n| --- | --- |\n| [`akter dev`](#akter-dev) | Local engine and read-only inspector. |\n| [`akter dev snapshots`, `akter dev environments`](#akter-dev-snapshots-and-akter-dev-environments) | Snapshot, branch and promote the local folder's environments, as on Akter Cloud. |\n| [`akter deploy`](#akter-deploy) | Cloud API build upload and deployment progress. |\n| [`akter migrate --all`](#akter-migrate) | Apply migrations to actors in a local folder. |\n| [`akter history`](#akter-history) | Read or verify a local actor's command journal. |\n| [`akter export`](#akter-export) | Download every actor's database and history from Akter Cloud into one `.akter` archive. |\n| [`akter import`](#akter-import) | Read an `.akter` archive into a local folder, checking every entry. |\n| [`akter bench`](#akter-bench) | Run a workload against `akter dev` or an Akter Cloud app and report every outcome. |\n| [`akter <group> <operation>`](#cloud-api-commands) | One command per Cloud API endpoint, such as `akter apps list`. |\n| [`akter login`, `akter signup`, `akter logout`](#signing-in) | Sign in or create an account with an emailed code, save or remove the login every Cloud command uses. |\n| [`akter docs`](#akter-docs) | Print these docs as Markdown. |\n\nOrganization, member, API key, app, build, deploy, domain, log, secret, account, session, usage and billing commands are [Cloud API commands](#cloud-api-commands). Calls to an app's actors are `akter actors call`, `query`, `state`, `watch`, `create`, `ensure` and [`fork`](#forking-an-actor); their files are `akter files` and their computers [`akter computers`](#akter-computers). Locally, [`akter dev snapshots` and `akter dev environments`](#akter-dev-snapshots-and-akter-dev-environments) snapshot, branch and promote. On Cloud, branch, restore and snapshot commands are not registered yet. Older operator commands aren't available either.\n\n## Shared app entry\n\n`dev`, `deploy`, `migrate` and `history` load `--entry`, defaulting to `src/app.layer.ts`. It must export the named value `app = { actors, layer }`. See the [checked deployment entry](../deploy/overview.md#write-the-server) for an example.\n\n## `akter dev`\n\n```sh\nbunx akter dev --entry src/app.layer.ts --dir .akter --port 4000\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--entry` | `src/app.layer.ts` | The module exporting `app`. |\n| `--dir` | `.akter` | The local actor folder. |\n| `--port` | `PORT`, otherwise `4000` | RPC listener port. |\n| `--inspector-port` | RPC port + 1 | Read-only inspector port; a zero RPC port selects a free inspector port. |\n| `--allowed-host` | Loopback names only | Additional allowed Host name; repeatable. |\n| `--reset` | Off | Ask before emptying the local actor folder. |\n| `--from` | None | An `.akter` archive to [import](#akter-import) into `--dir` first; `--dir` must not exist yet. |\n| `--environment` | The promoted one | Serve this environment of the folder, such as a quiet branch, instead of the one promoted to serve. |\n\nThe runner watches the entry for changes and restarts while keeping the folder. RPC is served at `/rpc`. Both listeners bind to loopback and check the Host header; an additional allowed name doesn't authenticate a caller. Don't expose the inspector publicly. See [Operating locally](../guides/operating.md#operator-access).\n\n## `akter dev snapshots` and `akter dev environments`\n\n```sh\nbunx akter dev snapshots create --name before-import\nbunx akter dev environments branch --name trial --snapshot before-import\nbunx akter dev --environment trial --port 4100 # call the quiet branch\nbunx akter dev environments promote --name trial\nbunx akter dev environments promote --name dev # promote back\n```\n\n| Command | Meaning |\n| --- | --- |\n| `snapshots create --name <n>` | Snapshot the serving environment, or `--environment`'s. The `akter dev` serving it takes it; until one does it stays `Pending`. |\n| `snapshots list`, `snapshots get --name <n>` | The environment's live snapshots. Ids are `snapshot-<n>`, numbered from 0 and never reused. |\n| `snapshots delete --name <n>` | Retire the snapshot and remove its copy. Its name is free again; its id isn't. |\n| `environments list`, `environments get --name <e>` | Every environment, deleted ones included, with `serving` and `quiet`. |\n| `environments branch --name <e> --snapshot <n>` | A new, quiet environment from a `Ready` snapshot of the serving environment, or `--from`'s. |\n| `environments promote --name <e>` | Serve `<e>` instead and lift its quiet. Promoting the one that serves changes nothing and prints the same result. |\n| `environments delete --name <e>` | Remove an environment that doesn't serve. `dev` and the serving one are refused; the name stays taken. |\n\nEvery command takes `--dir` and `--json`; with `--json` it prints the same shapes as Akter Cloud's `snapshots` and `environments` operations, and a refusal prints `code`, `reason` and `message`. The promote prints what it leaves out. See [Snapshots, branches and promote](../guides/operating.md#snapshots-branches-and-promote).\n\n## `akter deploy`\n\n```sh\nnpx @akterjs/cli deploy \\\n --organization \"$AKTER_ORGANIZATION\" \\\n --app \"$AKTER_APP\" \\\n --environment \"$AKTER_ENVIRONMENT\"\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--organization` | `AKTER_ORGANIZATION` | Existing organization. |\n| `--app` | Required | Existing app. |\n| `--environment` | `prod` | Provisioned destination environment. |\n| `--url` | `AKTER_CLOUD_URL`, then the saved login, then `https://api.akter.dev` | Cloud API base URL; HTTPS except for loopback development. |\n| `--entry` | `src/app.layer.ts` | The module exporting `app`. |\n| `--domain` | None | Claim this customer hostname for the environment and print the CNAME to add. |\n| `--json` | Off | Print the live deploy as one line of JSON in the API's `Deploy` schema, with its `hostname` and `url`; progress goes to stderr. |\n\nThe key comes from [signing in](#signing-in) and needs deploy scope for the organization. The key is exchanged for a short-lived bearer token; signed uploads don't receive that bearer. The CLI rejects redirects for token exchange, API requests and uploads.\n\nThe CLI uploads a new build or reuses a matching Ready one, confirms Ready, creates a new deployment and prints its id. Persisted notices are printed once each in order until `Live`, `NotServing` or `RolledBack`. `Live` prints `Actors at https://<hostname>`, the app's own hostname for that environment, where an Akter client calls its actors, and exits 0; `NotServing` prints why the published build isn't serving and exits nonzero; `RolledBack` exits nonzero. Build confirmation and deployment following have five-minute and thirty-minute deadlines respectively. Interrupting or timing out the CLI doesn't cancel the remote deployment.\n\n`akter deploy` doesn't create the organization or the app: `akter organizations create` and `akter apps create` do. An upload that succeeds doesn't by itself mean the deployment goes live; wait for `Live`.\n\nThere is no region flag. See [Deploying](../deploy/overview.md).\n\n## `akter migrate`\n\n```sh\nbunx akter migrate --all --dry-run --entry src/app.layer.ts --dir .akter\n```\n\n`--all` is required; migrating one actor from the CLI isn't implemented. Without `--dry-run`, pending migrations are applied now rather than on the actor's next open. Output names each actor's old and new migration version. A failure stops the command, so actors later in the traversal aren't migrated by that run. This command operates on a local folder, not the Cloud API.\n\n## `akter history`\n\n```sh\nbunx akter history Note/lobby --entry src/app.layer.ts --dir .akter --json\nbunx akter history Note/lobby --entry src/app.layer.ts --dir .akter --verify\n```\n\nThe journal is read oldest first. `--json` emits one JSON object per command. `--since` accepts a whole number with `ms`, `s`, `m`, `h` or `d`; a retained-history boundary is printed when the journal doesn't cover the full history.\n\n`--verify` replays the kept history against the current handlers and reports either no difference or the first mismatching outcome. It can't be combined with `--since`, and it refuses a replay whose initial history has been pruned. To verify a Cloud app's actors, export the app, import the archive and run `--verify` on the folder ([Use the exported data](../guides/exporting.md#use-the-exported-data)). See [History and replay](../core/history-and-replay.md).\n\n## `akter export`\n\n```sh\nbunx akter export \\\n --organization \"$AKTER_ORGANIZATION\" \\\n --app \"$AKTER_APP\" \\\n --url \"$AKTER_CLOUD_URL\" \\\n --out ./chat.akter\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--organization` | `AKTER_ORGANIZATION` | Existing organization. |\n| `--app` | Required | The app whose actors to export. |\n| `--url` | `AKTER_CLOUD_URL`, then the saved login, then `https://api.akter.dev` | Cloud API base URL; HTTPS except for loopback development. |\n| `--out` | Required | The `.akter` archive to write; it must not exist yet. Run again with the same path to resume. |\n\nThe key comes from [signing in](#signing-in) and needs export scope for the organization; a key with only deploy, call or read scope is refused. Every listing, snapshot and download request is authorized for that organization's app, and none of them reach another app or organization.\n\nAn export is one `.akter` file: a tar archive compressed with zstd (ADR 0019). Its first entry is `manifest.json`, which records the format version (1), when it was written, the size and SHA-256 of every other entry, the `computers` section with each computer the app's actors keep (its disk and checkpoint stay in Akter Cloud, so the checkpoint is marked `\"portable\": false`), and the `lineage` section, with one entry per forked actor: the actor, the actor it was forked from (`parent`), the fork's id (`fork`) and the parent's commit version at the cut (`forkedAt`). The entries after it are:\n\n- `shards/<shard>/<Actor>/<key>/actor.db` for each actor that has a database, in the layout `akter dev` uses, so `bunx akter dev --from ./chat.akter --dir ./chat` serves the exported actors.\n- Each `actor.db` is a plain, decrypted SQLite database that stock `sqlite3` opens. It holds the actor's whole database at one consistent point after its last committed command: its state, its own tables, its command journal (the history `akter history` reads), and its pending timers, outbox and jobs.\n- `shards/<shard>/<Actor>/<key>/files/<sha256>` for each distinct file the actor's database names, its plaintext content named by its SHA-256. The paths, sizes and times of the actor's files stay in its `actor.db`.\n- Nothing else. It holds no Cloud keys, no export records and no file keys: the CLI blanks every file grant in `actor.db` and rewrites the file before it archives it.\n\nAn erased actor has nothing to export: the CLI prints `<Actor>/<key>: erased, nothing to export` and goes on.\n\nEach actor is snapshotted on its own, after a commit and while no command runs on it, so its database is consistent. Actors are exported one after another, so two actors' snapshots come from different moments. The snapshot stays encrypted on the server under a key of its own, and only the download decrypts it. When the snapshot is taken, its completion record gets a MAC over every page in order. The server checks that MAC in the same pass that computes the snapshot's SHA-256, before it reports the snapshot, so a page rolled back to an earlier copy of itself stops the export with a gone-snapshot error instead of exporting stale data. That check covers the snapshot file only. The live `actor.db` on the server is authenticated page by page, so a page rolled back inside it is outside what export can detect, and a snapshot taken afterwards exports the rolled-back page as it is. Starting an actor's export replaces that actor's previous snapshot, so run one export of an app at a time.\n\nThe CLI downloads into a staging folder beside the archive, `<out>.staging`, at most 64 KiB per request, and prints each actor's progress every MiB and when it's done. It downloads an actor's files first, each into `files/<sha256>.export` and checked against its SHA-256 before it's renamed, then its database, which it checks against the SHA-256 the server computed when it took the snapshot, strips of file grants and only then renames to `actor.db`. While an actor downloads, `actor.db.export` holds the database's bytes so far and `actor.db.export.json` the snapshot they come from. A finished file is kept for the next run, and a file the actor's snapshot doesn't hold is removed from `files`. After an interruption, run the same command with the same `--out`: finished actors are kept, and a partly downloaded actor continues from the same snapshot. When every actor is in, the CLI writes the archive as `<out>.partial`, renames it to `<out>`, removes the staging folder and prints `Wrote <out> with <n> entries`. An export is complete only when it prints that line. If that snapshot was replaced, failed authentication on the server or doesn't match its SHA-256, the CLI deletes the partial download and exits nonzero; run it again to take a new snapshot.\n\nSee [Exporting an app](../guides/exporting.md) for a walkthrough.\n\n## `akter import`\n\n```sh\nbunx akter import ./chat.akter --dir ./chat\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `<archive>` | Required | The `.akter` archive to read. |\n| `--dir` | `.akter` | A new folder to read the actors into; it must not exist yet. |\n\nThe CLI reads the archive as a stream into `<dir>.importing`, checks every entry against the size and SHA-256 its manifest records, takes each actor's `files/<sha256>` into the folder's file home, `files-home`, under the actor's local identity, and renames the folder to `--dir` only once every entry has passed and every file is in. It refuses an archive whose first entry isn't `manifest.json`, that holds an entry its manifest doesn't list, whose entry doesn't match its checksum, that was cut short, that a newer akter wrote, or whose files can't be taken in, and then leaves no folder behind. Every format version akter has written stays readable. `akter dev`, `akter history` and `akter migrate` read the imported folder.\n\n## `akter bench`\n\n```sh\nbunx akter bench tests/sale.workload.ts --duration 30s --concurrency 8\nbunx akter bench tests/doc.workload.ts --duration 60s --json\n```\n\nThe workload module exports `workload = ({ id, kind, url }) => ({ name, open, step })`. `id` names this run, `url` is the target and `kind` says what it is: `\"local\"` for `akter dev`, `\"cloud\"` for an Akter Cloud app and `\"comparison\"` for another system, so a workload sends a development identity such as the `akter-principal` header only when `kind` is `\"local\"`. `open` is an Effect that needs an Akter client and returns a session, and `step(session)` is one operation whose success value names its outcome. Without `--url`, the command starts `akter dev` with `--entry` on an empty temporary folder and stops it when the run ends. A loopback `--url` reuses a running `akter dev`. Any other `--url` is an Akter Cloud app: the command signs in with `AKTER_API_KEY` or the key `akter login` saved, and keeps the run signed in past the 15-minute token. `--system <name> --url <url>` runs a workload against another system for comparison and signs in to nothing.\n\n`--concurrency` workers (8 by default) each run one step at a time and start no new step after `--duration` (30s by default). A step, or the workload's `open`, with no answer within `--step-timeout` (30s by default) counts as `failed: no answer within <ms> ms` and the worker moves on, so one stuck step can't hold the run open. The report has one row per outcome, and a failed step counts as `failed: <error tag>`, so failures and losses stay in the table. Each row and the total give steps, steps per second over the whole run, and p50, p90, p99 and max latency in milliseconds. Cost per row is always `unmeasured`.\n\nA local run's output starts with a `LOCAL RUN` notice: the numbers describe this machine and this checkout, not Akter Cloud. A Cloud run's starts with `AKTER CLOUD RUN`: every latency includes the network path from the machine that ran the bench to the app. The report also names the hardware, the runtime, the versions of `@akterjs/cli`, `@akterjs/client`, `@akterjs/effect` and `effect`, the git commit and whether the worktree is clean, and the SHA-256 of the nearest lockfile. `--json` prints the same report as one JSON object, with `\"local\": true` for a local run and `false` for a Cloud run.\n\n## Operator commands\n\nThe old `inspect`, `log`, `snapshot` and `retention` command groups aren't registered. Local inspection is the read-only listener started by `dev`; see [Operating locally](../guides/operating.md).\n\n## Forking an actor\n\n`akter actors fork --url <app url> --actor <Actor> --key <key> --into <new key>` copies an actor into a new actor at another key: its state and files at one moment, with none of its work in flight, since its jobs, timers and undelivered messages stay with the original. It prints the original's commit version at the cut. A retry with the same `--command-id` makes one copy, and `--dry-run` prints the request without sending it. There is no top-level `akter fork`; don't use the old history-copying examples against a 1.0 actor folder.\n\nOn the local engine the copy also gets each of the actor's computers: its disk as it was at the cut, stopped, so no process the original ran runs in the copy. While it copies them the fork holds the original's computers paused under the fork's id: a step already running finishes first, and one sent meanwhile fails with `ComputerHeldError` naming the fork's id. Then each computer goes back to where it was. If the engine stops partway through a fork, the hold stays until a retry with the same id finishes the fork. A fork whose copies would pass the store's limit of 4 computers is refused with `InvalidRequestError`. Akter Cloud copies each computer by its disk alone too, but to do that it stops the original's computer first, so one that was running starts again from its disk with its memory and processes gone, and one that was paused stays stopped. A copy that fails keeps the hold until a retry with the same fork id copies it. Akter Cloud refuses, with `ActorUnavailableError`, a fork whose copies would pass the environment's limit of 4 computers, and a fork with computers into a key on another shard.\n\nWhere the fork runs depends on the target, the same rule every actor command and `akter bench` use. A loopback `--url` (or `AKTER_APP_URL`), such as the address `akter dev` prints, is the local engine: the command sends the actor's fork RPC, `<Actor>/fork`, with no sign-in and no question. Any other target is Akter Cloud, where forking is an operator action (decision 5): the gateway refuses the fork RPC for every caller, so the command runs the Cloud API operation `actorForks.create` instead, `POST /v1/organizations/{organization}/apps/{app}/environments/{environment}/actors/{actor}/{key}/forks` with `into` and `forkId`:\n\n```sh\nakter actors fork --organization acme --app shop --environment prod --actor Sale --key s1 --into s1-try\n```\n\nIt signs in with your credentials (`AKTER_API_KEY` or `akter login`) at the Cloud API (`AKTER_CLOUD_URL` or the URL `akter login` saved); `--organization` defaults as for every Cloud command, and `--command-id` is sent as the fork id. Like every dangerous Cloud command it asks the person at the terminal to confirm, and exits 4 when no terminal can ask. An API key's fork also needs its creator's approval: at a terminal the command asks for the emailed code and retries once with the token, and without one it takes `--approval <token>` from `akter approvals request` and `akter approvals confirm`. The key needs the `fork` scope. On the hosted MCP server the same fork is the `actors_fork` tool, which takes `organization`, `app`, `environment`, `actor`, `key`, `into` and `forkId` and asks the person before anything is sent. The Effect and Promise clients' `fork(into)` stays an actor RPC, so it forks on `akter dev` and is refused on Akter Cloud by design.\n\nCloud forks are a preview behind `cloudFork`, default off. The CLI leaves `actors fork` out of help but still accepts it by name; control refuses an organization without the flag. MCP currently hides `actors_fork` from everyone until it can read organization flags, without falling back to the forbidden actor RPC. Local forks do not use a flag.\n\n## Cloud API commands\n\nEvery Cloud API endpoint is one command, `akter <group> <operation>`, named after the API's own groups and operations: `akter apps list`, `akter apps create`, `akter deploys get`, `akter members change-role` and so on. `akter --help` lists the groups and `akter <group> --help` their operations. The same list, with the same names, descriptions, input schemas and safety hints, is what Akter's MCP tools are built from, so a command and its tool always agree.\n\n- Each field of the operation's input is a flag, such as `--organization` and `--name`. `--input '<json>'` takes the whole input as one JSON object, the same arguments the MCP tool takes; `--input @file.json` reads a file and `--input -` reads stdin. Flags override its fields. Pass a secret, such as a `secrets set` value, with `--input -` or `--input @file.json` rather than a flag, so it stays out of the process list and shell history; `--dry-run` prints the body, secret included. The CLI checks the input against the operation's schema before it sends anything and exits 2 when it doesn't match.\n- `--json` prints the API's answer as one line of JSON in the API's own schema, after checking it against that schema; an answer that doesn't match exits 1, because the CLI and the API disagree. Without `--json` the answer is printed indented. A refusal prints the API's error body with `--json`, and one line on stderr otherwise. After a 503 on a change that is safe to repeat, such as `akter actors erase`, that line adds that it may have stopped partway and to run the same command again.\n- A read that pages takes `--after` and answers with `next` while there is more: `akter actors list`, which lists an app's actors by name and key (`--actor Room` for one name, `--environment` for an environment other than `prod`), and `akter actors history`. An `akter actors list` page holds `--limit` actors (500 at most, and by default) unless it is the last, and the last page has no `next`, so read on until `next` is absent. `--all` does that: it sends each page's `next` as `--after` until a page has none and prints one answer in the API's schema with every page's entries and no `next`. A refused page stops it with that page's error and exit code.\n- Commands that change something take `--dry-run`, which checks the input and prints the request instead of sending it.\n- Destructive commands, such as `akter apps delete`, ask a person to confirm. When stdin or stdout isn't a terminal they never ask: they exit 4 at once, unless `--approval <token>` carries an approval for the action.\n- Signed in with an API key, a dangerous action also needs its creator's approval, and the API refuses it with 428 `ConfirmationRequiredError` until a token comes with it. At a terminal the CLI asks the API to email the key's creator a code, asks you to type it, turns it into a single-use token and sends the command once more; it never prints the token. A refused code exits 3. Without a terminal it exits 4 with reason `ApprovalRequired` and names the steps: `akter approvals request` emails the code, `akter approvals confirm` turns it into a token, and the command runs again with `--approval <token>`.\n- `--organization` defaults to `AKTER_ORGANIZATION`, then the organization saved by `akter login`; `--url` to `AKTER_CLOUD_URL`, then the saved URL, then `https://api.akter.dev`.\n\n## `akter computers`\n\n`akter computers <list|get|logs|pause|resume|stop|delete> --url <app url> --actor <Actor> --key <key>` reaches one actor's computers over its `<Actor>/computer/<op>` RPCs, and each operation is also the `computers_<op>` MCP tool. Every operation but `list` takes `--computer <name>`. `logs` prints a page of the computer's log, oldest first, each line with its `seq`; `--after <seq>` reads on after the last line printed and `--limit` takes 1 to 1000 lines. Each computer's lines also reach the app's log with source `computer` and subject `<environment>/<Actor>/<key>/<computer>`, so `akter logs follow --app <app> --source computer` follows every computer of the app at once. `pause` keeps memory and disk, `stop` keeps the disk, and `resume` fails while a fork holds the computer. There is no command to make a computer or run one of its commands: only the actor's own jobs do that.\n\n`delete` removes the computer and its disk, so it asks a person to confirm, exits 4 when stdin or stdout isn't a terminal, and prints the request without asking under `--dry-run`.\n\n## `akter logs follow`\n\n`akter logs follow --app <app>` prints an app's log lines as they arrive, oldest first, one per line: when, the level, what wrote it and what it's about, then the message. It keeps reading after the last line it printed and waits two seconds when nothing new has come. It runs until interrupted, or until `--count <n>` lines have printed. `--deploy`, `--since` and `--source` narrow it as they do `akter logs read`; the sources are `deploy`, a deploy's progress with its id as the subject, and `computer`, what happened to one of the app's computers. `--json` prints each line as one line of JSON in the API's `LogLine` schema.\n\nIt never prompts. A timeout, a dropped connection, 429 or 5xx is retried with backoff, and each retry is noted on stderr. Any other refusal ends it with the exit code below, such as 1 for an app that doesn't exist and 3 for a key without access.\n\n## Signing in\n\nEvery Cloud command, `deploy` and `export` included, signs in the same way:\n\n1. `AKTER_API_KEY`, an API key or an agent key, when it is set and not empty. Scripts, CI and agents use this. When `AKTER_CLOUD_URL` is set too, the key goes only to that origin: a `--url` naming another server exits 2 with `ApiKeyForOtherUrl`, naming both origins, instead of sending the key there. Drop `--url`, or point `AKTER_CLOUD_URL` at the server you mean.\n2. Otherwise the session or key `akter login` saved. It goes only to the Cloud URL it was saved for: a `--url` or `AKTER_CLOUD_URL` that names another server exits 3 with `LoginForOtherUrl` instead of sending it there. Sign in for that URL, or set `AKTER_API_KEY`.\n\n`akter login --email <address>` emails a six-digit code and asks for it when a person is at the terminal; `akter signup` is the same, for an address with no account yet, since the first sign-in creates it. Without a terminal, as an agent runs it, the command emails the code, prints how to finish (with `--json`, `{ \"codeSent\": true, \"email\", \"expiresAt\" }`) and exits 0 without waiting; `akter login --email <address> --code <code>` then finishes. A refused code exits 3 and saves nothing; `--email` missing without a terminal exits 2. A code that creates an account first needs Akter's terms of service and privacy notice accepted, which `akter docs legal/terms` and `akter docs legal/privacy` print. The CLI asks a person at the terminal once, naming their version, and then sends the same code again; `--accept-terms` accepts them without asking, for an agent. Without a terminal or the flag, it exits 4, names the flag and saves nothing, and the code still works for the next run. A sign-in to an existing account is never asked. The session's secret is saved like a key, below, and never printed: `--json` prints the account, whether this sign-in `created` it, the session's id and expiry, and where it was saved.\n\n`akter login --with-api-key --url <cloud-url>` saves a key instead: it reads the key from stdin or a hidden prompt, never from a flag, checks it with the Cloud API and saves it. The key goes to the OS keychain (macOS Keychain, or the Secret Service through `secret-tool` on Linux) when one is reachable, and otherwise into `$XDG_CONFIG_HOME/akter/login.json` (`~/.config/akter/login.json`), which only you can read (mode 0600). `akter logout` removes it.\n\nWithout either, a Cloud command exits 3.\n\n## `akter docs`\n\n`akter docs` lists the topics of these docs, and `akter docs <topic>` prints one page as Markdown, such as `akter docs reference/cli`, or `akter docs cli` when only one page has that name. `--json` prints the list, or the page with its topic and title, as one line of JSON. An unknown topic exits 2 and lists the topics that contain it.\n\n## Exit codes\n\n| Code | Meaning |\n| --- | --- |\n| 0 | Done. |\n| 1 | Failed: the API refused (404, 409 and the like), the answer broke its schema, or a command such as `deploy` ended without success. |\n| 2 | Usage: an unknown flag, a missing or invalid input, or a 400 or 422 from the API. |\n| 3 | Authentication: not signed in, the API answered 401 or 403, including when it refused the key at sign-in, or an actor refused the caller, by its kind (`CallerNotAllowedError`) or by its `files.authorize` rule (`FileAccessDeniedError`). |\n| 4 | Confirmation: a person had to confirm or approve and couldn't be asked, or declined; also a 428 from the API. |\n| 5 | Unavailable: the API couldn't be reached, timed out, or answered 408, 429 or 5xx, including at sign-in. Retrying may help. Every Cloud command's sign-in first retries a 5xx for about 30 seconds, so it rides out a Cloud deploy. |\n| 130 | Interrupted. |\n\n`deploy` and `export` use the same codes when the API answers with a status, and 1 when it can't be reached; signing in, their first step, exits 3 or 5 as above. Treat a nonzero exit as failure and retain its diagnostic. With `--json`, a failure the CLI detects itself prints one JSON object with `code`, `reason` and `message`. Help exits successfully, deployment success requires the terminal `Live` notice, and an export is complete only when it prints `Exported <n> actors to <folder>` and exits 0. An uploaded build, a printed deployment id or an interrupted follow isn't equivalent to a successful deployment.\n",
|
|
127
|
+
"topic": "reference/cli"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"markdown": "# Errors\n\nA command or query can fail with an error declared by its contract, an Akter error, or a transport error. The Effect client puts these in its typed error channel. The Promise client rejects with the error; see [Calling actors](../guides/client.md).\n\nCatch the error you can handle. Don't turn an unavailable actor or a lost connection into a successful business result.\n\n## Declare an error\n\nThis counter refuses to go above three:\n\n```ts title=\"counter.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport class LimitExceededError extends Schema.TaggedError<LimitExceededError>()(\"LimitExceededError\", {\n limit: Schema.Int,\n}) {}\n\nexport const Counter = Akter.make(\"Counter\", {\n key: Schema.String,\n state: Schema.Struct({ count: Schema.Int }),\n initial: () => ({ count: 0 }),\n commands: {\n add: Akter.command({\n payload: { by: Schema.Int },\n success: Schema.Int,\n error: LimitExceededError,\n }),\n },\n queries: {\n count: Akter.query({ success: Schema.Int }),\n },\n})\n```\n\nThe handler returns the declared error through Effect's error channel:\n\n```ts title=\"counter.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Counter, LimitExceededError } from \"./counter.contract.ts\"\n\nexport const CounterLive = Counter.toLayer({\n commands: {\n add: (counter, { by }) =>\n Effect.gen(function* () {\n const count = counter.count + by\n if (count > 3) {\n return yield* new LimitExceededError({ limit: 3 })\n }\n yield* Counter.update({ count })\n return count\n }),\n },\n queries: {\n count: (counter) => Effect.succeed(counter.count),\n },\n})\n```\n\nA declared command failure rolls back its state and SQL changes. The failure itself is recorded in the command journal; it isn't a successful command. See [Testing](../guides/testing.md) for a rollback assertion that checks a table too.\n\n## Handle one error\n\nThis script calls through the HTTP client. The first add succeeds. The second would exceed the limit, so `Effect.catchTag` handles that error and leaves every other failure alone:\n\n```ts title=\"try-errors.ts\"\nimport assert from \"node:assert/strict\"\nimport { Effect, Layer } from \"effect\"\nimport { HttpServer } from \"effect/http\"\nimport { Akter } from \"@akterjs/effect\"\nimport { akterClient } from \"@akterjs/client/effect\"\nimport { Counter } from \"./counter.contract.ts\"\nimport { CounterLive } from \"./counter.actor.ts\"\n\nconst server = Akter.serve({ actors: [Counter], port: 0 }).pipe(\n Layer.provide(Akter.local(\".akter\").pipe(Layer.provide(CounterLive))),\n)\n\nconst client = Layer.unwrap(HttpServer.addressFormattedWith((url) => Effect.succeed(akterClient({ url }))))\n\nconst program = Effect.gen(function* () {\n const counter = yield* Counter.client(\"clicks\")\n assert.equal(yield* counter.add({ by: 1 }), 1)\n const answer = yield* counter.add({ by: 4 }).pipe(\n Effect.map((count) => `count ${count}`),\n Effect.catchTag(\"LimitExceededError\", (error) => Effect.succeed(`limit ${error.limit}`)),\n )\n assert.equal(answer, \"limit 3\")\n const count = yield* counter.count()\n assert.equal(count, 1)\n console.log(`${answer}; stored count ${count}`)\n})\n\nEffect.runPromise(program.pipe(Effect.scoped, Effect.provide(client.pipe(Layer.provideMerge(server)))))\n```\n\n```text output\nlimit 3; stored count 1\n```\n\n`LimitExceededError` is decoded from the contract on the client; no string parsing or generic `ActorError.reason` is needed.\n\n## Akter errors\n\nThese error classes are exported by `@akterjs/effect/contract/errors` and re-exported by `@akterjs/client/effect`:\n\n| Error | Meaning |\n| --- | --- |\n| `ActorNotCreatedError` | The actor requires explicit creation and hasn't been created. |\n| `ActorAlreadyExistsError` | An explicit `create` tried to create an actor that exists. Use `ensure` when that is intended. |\n| `CallerNotAllowedError` | The caller's kind may not call this member. The error carries `actor`, `member` and the refused `kind`, and its message shows the `callers` declaration that admits that kind ([Access](../core/access.md#admit-api-keys)). This doesn't replace checks on which key that caller may access. |\n| `UnauthenticatedError` | The listener couldn't establish the caller. |\n| `ActorUnavailableError` | The actor couldn't be opened or served; read its message rather than assuming the command ran. |\n| `InvalidRequestError` | The host refused the request as invalid. |\n| `CommandIdExpiredError` | The command id predates the journal's retained deduplication window. |\n| `HandlerFailedError` | The handler failed outside its declared error contract. Check the handler; don't treat this as a business refusal. `platform: true` means the failure was Akter's, such as the actor's files being unavailable or its SQLite storage failing (an I/O, full, read-only, locked or corrupt database), not the handler's; a constraint or syntax error, or a write in a query, stays the handler's own; retry it, and Akter Cloud doesn't bill it. |\n| `SpendCapReachedError` | Akter Cloud refused new spend because the organization's or the API key's monthly spend cap is reached (`scope`). Nothing ran; a person raises the cap. |\n| `StorageLimitError` | Akter Cloud refused a command, creation or upload because the app environment's stored data reached its ceiling (`dataBytes` of `ceilingBytes`, read at `readAt`). Nothing ran. Queries and downloads still work; a write succeeds again once data is removed or the ceiling is raised. |\n| `FileTooLargeError` | An upload would pass the largest file the server accepts (`maximumBytes`, 1 GiB unless the host sets its own). The server dropped the upload and nothing was attached; split the content into smaller files. |\n| `BillingUnavailableError` | Akter Cloud couldn't confirm spend fits under the caps for too long to admit it. Nothing ran; retry after `retryAfterSeconds`. |\n\nNot every member can return every error above. The client's return type describes that member's failures. `RpcClientError` comes from Effect's RPC transport, not the actor contract.\n\n## Retry boundaries\n\nCommands and explicit creation calls automatically retry `RpcClientError` up to five times, starting at 50 ms and doubling the delay. Those attempts reuse the command id, so a lost reply doesn't turn the retry into another command. Queries don't retry, and declared errors and Akter errors aren't retried by this policy.\n\nAfter a transport failure, don't assume the command didn't commit. Starting a new call generates a new command id; it isn't the same retry. See [History and replay](../core/history-and-replay.md) when you need to inspect what was recorded.\n\nThe 0.3 `ActorError` reason table and ten-minute retry policy don't describe this client.\n",
|
|
131
|
+
"topic": "reference/errors"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"markdown": "# Guarantees\n\nThis page lists what an Akter engine promises. Each promise is a test in the conformance suite, [`@akterjs/conformance`](../../../../packages/conformance/README.md), and each one below links to the test that proves it, by the test's title. An engine counts as an Akter engine when it passes the suite.\n\n## What Akter guarantees\n\n- **A reply means the command committed.** Akter replies to a command only after the command's transaction has committed to the actor's SQLite database, so an engine restarted on the same storage sees every command that got a reply. Test: [a reply means the command committed: an engine reopened on the same storage sees every replied command](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A command runs once per command id.** Akter records each command's outcome under its command id. A retry with the same id gets the recorded reply, a success or a declared error, without running the handler again, and that holds after a restart too. Test: [a retried command id returns the recorded outcome and runs the handler once](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A failed command leaves nothing behind.** When a handler fails, with its declared error or with a defect, Akter rolls back the command's SQL writes and its state change. The next command starts from the state before the failure. Test: [a handler failure rolls back every write the command made](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Caller checks run before the handler.** A caller whose kind the actor doesn't declare, or that the command's `callers` list leaves out, gets `CallerNotAllowedError`, and the handler doesn't run. Test: [caller checks run before the handler: a caller the command doesn't allow changes nothing](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Migrations run when an actor opens.** An actor's migrations run before its first command or query, and a migration added in a later release runs the next time the actor opens, keeping the data that's there. Test: [migrations run when an actor opens, before its first call, and a migration added later runs on the next open](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Queries never write.** A query reads the actor's last committed state and tables, and a write statement inside a query fails, so a query leaves the actor as it found it. Test: [queries never write: a write inside a query fails and leaves the actor unchanged](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Akter's own tables are read-only to handlers.** The `akter_*` tables in an actor's database hold its state, journal, timers and outbox, and only the engine writes them. A handler may read them, but a write to one fails with an error that names the table, and the command rolls back like any failed command. Test: [a handler may read akter_* tables but never write them: the write fails, names the table and changes nothing](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A file call is decided by the actor's rule.** An actor declares which caller kinds may make file calls, and its `files.authorize` rule decides each call from the actor's state and the caller the engine verified. A kind the declaration doesn't list gets `CallerNotAllowedError` before the rule runs, and a payload that names another caller changes nothing. Test: [a file call is decided by the actor's rule from the verified caller and state: a kind it doesn't declare is refused, and a payload naming another caller changes nothing](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Live queries update only after a commit.** A subscriber to a query gets its current result, then one update for each committed command that changed it. A command that fails sends no update, so a subscriber never sees a write that rolled back. Test: [live queries update only after a commit: a subscriber sees each committed change and nothing from a rolled-back command](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A sent message is delivered after its command commits.** A message a command sends with `Akter.send` is saved in the command's transaction and delivered only after that transaction commits. Akter retries delivery until the target takes it, after a restart too, and the target runs it once because the message carries its own command id. A command that fails sends nothing. Test: [a message a command sends is delivered after the command commits, at least once, and a rolled-back command sends none](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A command's signal reaches its job after the command commits.** A command that signals one of its own actor's jobs with `Akter.signal` (`ctx.signal` in the plain SDK) saves the signal in its transaction, and the waiting job wakes only after that transaction commits. A command that fails signals nothing, a retried command signals once, and a job that has already finished ignores the signal. Test: [a command's signal reaches its own actor's job after the command commits, once: a rolled-back command signals nothing and a finished job ignores it](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A job keeps the code its start declared.** A job declares named patches (`patches` on `Akter.job` and the plain SDK's `job`), and its run asks `patched(name)` (`steps.patched` in the plain SDK). A job a command started while the contract declared the patch sees it for its whole run; a job started before the deploy that added it doesn't, even though it resumes on the new code. Test: [a job keeps the code its start declared: after a deploy adds a patch, a job started before it takes the old path and one started after takes the new](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A timer fires once.** A timer set with `Akter.schedule` is saved with the command that set it and runs its command once it's due. It doesn't fire early, and it doesn't fire again, even after the engine restarts. Test: [a timer fires once at its due time, and not again after the engine restarts](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A fork copies an actor at one cut and starts quiet.** A fork makes a new actor of the same kind, under a key you choose, from what the parent had committed at one point between two of its commands. It keeps none of the parent's work in flight, so a timer the parent had pending never fires in the copy, and the parent still fires its own. After the cut, a command on either one changes only that one, and both are still there after a restart. Forking again with the same fork id answers the same fork instead of copying the parent a second time, so the copy keeps what it did since. Test: [a fork copies an actor as committed at its cut and starts quiet: the copy keeps no pending timer, the two change independently, and a retried fork id makes one copy](../../../../packages/conformance/src/fork/fork-conformance.suite.ts).\n- **A fork copies the actor's computers at the cut, stopped.** Each computer the parent keeps is copied into the fork under the same name, from its disk as it was at the cut: the copy has the files written before the cut and runs none of the parent's processes, so it starts stopped and a step on it starts it again. The parent's computers go back to where they were, a running one running and a stopped one stopped, and after the fork a step on either one changes only that one. Test: [a fork copies each of the actor's computers by its disk at the cut, stopped: the copy holds the files written before the cut, the original's computers go back to where they were, and the two change independently](../../../../packages/conformance/src/fork/fork-conformance.suite.ts).\n- **A computer step runs once.** A job activity runs a step on its actor's computer with `exec` and a key, and the computer records the step's outcome under that key. Sending the same step again, after a lost reply or an engine restart, reads that outcome back instead of running the command a second time. Test: [a computer step runs once by its key: sent again, even after the engine restarts, it reads back the outcome it recorded](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A durable step is journaled in its actor.** A job runs a durable step with `step` on its actor's computers, and the step records where it runs, its key and when it started in the actor's journal before it sends anything, then its outcome. The actor's steps read back newest first, and one step by its key with its output. Test: [a durable computer step journals its start and outcome in the actor, and its steps read back newest first, one with its output](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A durable step cut off by a restart never runs again.** When the engine stops while a durable step runs, the replayed job sends the step's key again: it reads back the outcome the computer recorded, or, once the step's time limit has passed without one, settles it as unknown, because the command may already have done its work. Either way the command ran at most once. Test: [a durable computer step cut off by an engine restart never runs again: its replayed job settles it with its recorded outcome or unknown](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A computer keeps its files.** What a step writes on a computer's disk is still there after the computer is paused, resumed and stopped, and after the engine restarts. A step on a paused or stopped computer starts it again first. Test: [a computer keeps its files across pause, resume, stop and an engine restart](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A held computer stays paused, or stopped if it was stopped.** A pause with a hold, as a fork takes one, keeps the computer paused, and a stopped computer stopped: `ensure`, a step, and a `resume` or `pause` with no hold or another hold fail with `ComputerHeldError`, and a refused step doesn't use up its key. The resume with the same hold returns the computer to where the pause found it: running, paused or stopped. Test: [a held computer stays paused, or stopped if it was stopped: an ensure, a step, and a resume or pause without its hold are refused until its hold resumes it](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A computer past the store's limit is refused.** Each app environment keeps a bounded number of computers. One more fails with `ComputerLimitError`, which names the limit, and makes nothing; once a computer is removed, a new one fits again. Test: [a computer past the store's limit is refused with ComputerLimitError and makes nothing, and one fits again once another is removed](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A removed computer is gone.** Removing a computer deletes its disk. Calls to it fail with `ComputerNotFoundError`, removing it again changes nothing, and ensuring the same name starts a new, empty computer. Test: [a removed computer is gone with its disk: calls to it fail with ComputerNotFoundError, and ensuring it again starts it empty](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n\n## How it's tested\n\nThe suite runs each guarantee against a fresh engine, and restarts the engine on the same storage where a guarantee needs a restart. It drives the engine only through the actor host, with the same JSON payloads and replies a client sends, so it runs against any engine. The local engine, `Akter.local` on a folder, runs the suite in `pnpm check` on Bun and on Node. The fork guarantees and the computer guarantees are suites of their own; the local engine runs computers, and passes them and the fork guarantee with computers, only with `AKTER_LOCAL_COMPUTERS=true`, because its computers run steps unsandboxed on your machine. Without it, every computer call answers `ComputersUnavailableError`.\n\nA test in the suite checks that every guarantee on this page links to a test with the same title, and that every test in the suite is listed here. A promise appears on this page only once its test has landed.\n",
|
|
135
|
+
"topic": "reference/guarantees"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"markdown": "# Local engine and server\n\n`Akter.local(directory)` runs actors in SQLite on your machine. `Akter.serve({ actors })` adds the HTTP endpoint that the clients call. `Akter.localClient({ actors })` gives a script or test the same client without the endpoint: calls go through the same contract and caller checks in the same process, and nothing listens on a port. All three come from `@akterjs/effect`; contracts import `Akter` from `@akterjs/effect/contract` so they can also load in browsers.\n\nThis is the development listener, not a production authentication setup. It accepts a caller chosen by the request. Keep it on loopback; see [Access](../core/access.md).\n\n## Serve an actor\n\nA contract lists the calls:\n\n```ts title=\"counter.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport const Counter = Akter.make(\"Counter\", {\n key: Schema.String,\n state: Schema.Struct({ count: Schema.Int }),\n initial: () => ({ count: 0 }),\n commands: {\n add: Akter.command({ payload: { by: Schema.Int }, success: Schema.Int }),\n },\n queries: {\n count: Akter.query({ success: Schema.Int }),\n },\n})\n```\n\nProvide its handlers to the local engine, not to the client:\n\n```ts title=\"counter.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Counter } from \"./counter.contract.ts\"\n\nexport const CounterLive = Counter.toLayer({\n commands: {\n add: (counter, { by }) =>\n Effect.gen(function* () {\n const count = counter.count + by\n yield* Counter.update({ count })\n return count\n }),\n },\n queries: {\n count: (counter) => Effect.succeed(counter.count),\n },\n})\n```\n\nThis script starts the listener on a free port, calls it, then closes the client and engine when the Effect's scope ends:\n\n```ts title=\"try-server.ts\"\nimport { Effect, Layer } from \"effect\"\nimport { HttpServer } from \"effect/http\"\nimport { Akter } from \"@akterjs/effect\"\nimport { akterClient } from \"@akterjs/client/effect\"\nimport { Counter } from \"./counter.contract.ts\"\nimport { CounterLive } from \"./counter.actor.ts\"\n\nconst server = Akter.serve({ actors: [Counter], port: 0 }).pipe(\n Layer.provide(Akter.local(\".akter\").pipe(Layer.provide(CounterLive))),\n)\n\nconst client = Layer.unwrap(HttpServer.addressFormattedWith((url) => Effect.succeed(akterClient({ url }))))\n\nconst program = Effect.gen(function* () {\n const counter = yield* Counter.client(\"clicks\")\n console.log(yield* counter.add({ by: 2 }))\n console.log(yield* counter.count())\n})\n\nEffect.runPromise(program.pipe(Effect.scoped, Effect.provide(client.pipe(Layer.provideMerge(server)))))\n```\n\n```text output\n2\n2\n```\n\nThe folder survives the script. Running it again against the same folder prints `4` twice. `port: 0` asks the operating system for a free port; read the bound address through `HttpServer` rather than guessing it.\n\n## Listener options\n\n| Option | Default | Meaning |\n| --- | --- | --- |\n| `actors` | required | The contracts to serve; their handlers must be registered with the engine. |\n| `hostname` | `127.0.0.1` | The address to bind. A non-loopback listener fails with `ListenerNotLoopbackError` unless the unsafe option is set. |\n| `port` | `4000` | The TCP port, or `0` for a free port. |\n| `allowedHosts` | `[]` | Additional request Host names to accept. Loopback names are already allowed; `true` accepts any Host. This isn't authentication. |\n| `unsafeExposeDevelopmentAuth` | `false` | Allows a non-loopback listener and disables the loopback Origin check. Any caller can choose its principal. Don't use this for public access. |\n\nThe endpoint is `/rpc`, using HTTP and NDJSON. This development listener does not provide Cloud host ownership, fencing or operator access.\n\n## Engine lifetime\n\nProvide every actor's `toLayer` implementation to `Akter.local`; merge several with `Layer.mergeAll`. Serving a contract without registered handlers fails with `ActorsNotHostedError`. Keep the engine and listener in a scope so their resources close together.\n\n`akter dev` wires this up for the app you export; see [Getting started](../getting-started.md). For tests, [Story](../guides/testing.md) manages a temporary folder and clock. See [Migrations](../guides/changing-state.md) before changing stored tables or state, and [History and replay](../core/history-and-replay.md) for journal reads.\n",
|
|
139
|
+
"topic": "reference/server"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"markdown": "# Why Akter\n\n## The problem\n\nA backend that holds state usually ends up as a pile of parts: a database for the data, a queue for background work, a scheduler for timers, a pub/sub channel for live updates, and a lot of code to keep them in step. Each part fails in its own way, and the bugs live in the gaps between them. The reply goes out before the write lands. A retry charges the card twice. A deploy drops the job that was halfway done.\n\n## The model\n\nAkter starts from the things you're modeling. A chat room, an order, a document, an agent session: each has an id, some state, and a set of things that can happen to it. In Akter, each one is an actor.\n\nAn actor has one writer. Its commands are Effects that can read and write its SQLite tables, update its state, schedule timers and queue messages or jobs. External I/O belongs in [jobs](core/jobs.md), outside the command's transaction.\n\nAnything slow or uncertain, like an HTTP call, an email or a model completion, runs in a job. A job is an Effect, and each step's result is recorded as it finishes, so a job that crashes or retries picks up where it left off.\n\nThe command journal records handled commands and their outcomes. [History and replay](core/history-and-replay.md) uses that journal to check current handlers; actor state is stored directly, not rebuilt from events.\n\n## SQLite on Archil\n\nThe adopted Akter Cloud design keeps each actor's encrypted SQLite database and SQLite WAL directly on a checked-out Archil mount. One store disk holds an app in one environment. SQLite owns database recovery and checkpointing.\n\nA host must hold the shard's exclusive Archil checkout before opening its actors. That checkout is the ownership authority, not a lease or a SQLite file lock. A host that loses its checkout must stop serving the shard.\n\nThe Cloud design uses `synchronous=FULL` and one savepoint per command inside a batch transaction. Replies, live updates, outbox deliveries and job starts wait for the durable commit; the next batch's handlers wait too. Synchronous database work runs in a bounded worker pool rather than on the host's shared event loop.\n\nComputers have separate disks and never mount the store. Archil supplies whole-store branches and checkpoints, but an individual actor fork, export or restore still needs a coordinated copy of its database, files and relevant computer state.\n\nFor local development, `Akter.local` and `akter dev` use SQLite in a folder without a cloud account. That development engine is not a self-hosted Cloud replacement; see [Deploying](deploy/overview.md) for the current boundary.\n\n## Built on Effect\n\nAkter is built on [Effect](https://effect.website), and doesn't hide it. State and payloads are Effect schemas, jobs are Effects, client calls return Effects, live state is a Stream, and handlers are provided as Layers. If you already use Effect, Akter should feel familiar. If you don't, the plain SDK (`@akterjs/sdk`) declares the same actors with Standard Schema validators such as Zod and plain synchronous handlers, and runs on the same engine.\n\n## What it costs\n\nDurable commits require synchronization, and group commit shares that work across a batch. Cold-open measurements are workload-specific and do not establish a general latency guarantee.\n\nWhat one key sustains on Akter Cloud, measured end to end from a client: one flash-sale `Sale` with one key, where every command is a `buy` that commits an order row and a lower stock, each caller sending its next command once the last was answered:\n\n| Callers | Commands per second | p50 latency | p99 latency | Failed |\n| ------- | ------------------- | ----------- | ----------- | ------------ |\n| 1 | 12.1 | 58 ms | 742 ms | 0 of 725 |\n| 16 | 116 | 90 ms | 741 ms | 0 of 6,984 |\n| 64 | 141 | 439 ms | 1,050 ms | 0 of 8,470 |\n| 512 | 139 | 3,688 ms | 4,440 ms | 0 of 8,711 |\n\nFrom 64 callers on the rate stays at about 140 commands per second and more callers only wait longer.\n\nThe conditions, on 12 November 2026: a preview stage of Akter Cloud (akter-cloud 449887a9, host akter 3e49536), one host on an Archil sandbox in AWS us-west-2 with 2 vCPUs and 4 GB, the production batch settings (up to 4,000 commands a batch, SQLite `synchronous=FULL`), and the bench run from a 16-core machine in Oregon through the app's public URL, so every latency includes that network path. Each row is one 60-second run on a new key. Every command answered was committed, and every committed command was answered. Reproduce it by deploying [the flash-sale example](https://github.com/rika-labs/akter/tree/main/examples/flash-sale) to your own app and running [its ceiling workload](https://github.com/rika-labs/akter/blob/main/examples/flash-sale/bench/ceiling.workload.ts) from that folder once per caller count:\n\n```sh\nbunx akter bench bench/ceiling.workload.ts --url <app url> --concurrency 64 --duration 60s\n```\n\nCommands can use the actor's own database, but they cannot make external network calls. Split work that contacts another service into a command and a job activity.\n\nAkter is pre-1.0. The APIs still change between minor versions.\n\n## When to use something else\n\n- **Queries across actors.** A query reads one actor's database. Cross-actor reports need an index you maintain rather than a join across actor databases.\n- **Transactions that span actors.** A command changes one actor. Outbox messages are delivered at least once and deduplicated by command id, separately from the sender's transaction.\n- **A replayable event stream.** The command journal supports history and replay; it is not an event stream that rebuilds actor state.\n- **Exactly-once effects in other systems.** Pass stable idempotency keys to providers. Journaling a job activity cannot make a separate remote write atomic with the actor's commit.\n- **Long-running workflows.** Temporal and Restate are built around workflow runs, with cancellation and compensation. Akter's jobs borrow the recorded-step idea, but Akter is centered on actor state.\n\n## Compared to Durable Objects\n\nCloudflare Durable Objects have the same core idea: an object with an id and its own storage that handles one request at a time. Akter Cloud instead runs its SQLite hosts on Archil sandboxes. Akter's contracts and clients can run in Workers and browsers, but those runtimes are not the stateful Cloud host. The public SDK and local engine are separate from the private Cloud engine.\n",
|
|
143
|
+
"topic": "why-akter"
|
|
144
|
+
}
|
|
145
|
+
];
|
|
146
|
+
// src/docs/docs.config.ts
|
|
147
|
+
const noWidth = 0;
|
|
148
|
+
const listTopics = fnUntraced(function* listTopics(json) {
|
|
149
|
+
const topics = topicsOf(pages);
|
|
150
|
+
if (json) {
|
|
151
|
+
const text = yield* compactJson(topics);
|
|
152
|
+
return yield* log(text);
|
|
153
|
+
}
|
|
154
|
+
const width = reduce(topics, noWidth, (widest, { topic }) => Math.max(widest, topic.length));
|
|
155
|
+
yield* log("Topics (akter docs <topic>):\n");
|
|
156
|
+
return yield* forEach(topics, ({ title, topic }) => log(` ${padEnd(width)(topic)} ${title}`), { discard: true });
|
|
157
|
+
});
|
|
158
|
+
const unknownTopic = (query, near) => {
|
|
159
|
+
const names = map(near, ({ topic }) => topic);
|
|
160
|
+
const hint = match(names, {
|
|
161
|
+
onEmpty: () => " Run akter docs to list them.",
|
|
162
|
+
onNonEmpty: (found) => ` Did you mean: ${join(found, ", ")}?`
|
|
163
|
+
});
|
|
164
|
+
return CliExitError.make({
|
|
165
|
+
code: CliExitError.codes.usage,
|
|
166
|
+
message: `No docs topic "${query}".${hint}`,
|
|
167
|
+
reason: "UnknownTopic"
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
const showTopic = fnUntraced(function* showTopic(query, json) {
|
|
171
|
+
const page = yield* fromResult(mapError(lookup({
|
|
172
|
+
pages,
|
|
173
|
+
query
|
|
174
|
+
}), (near) => unknownTopic(query, near)));
|
|
175
|
+
if (!json) return yield* log(page.markdown);
|
|
176
|
+
const text = yield* compactJson({
|
|
177
|
+
markdown: page.markdown,
|
|
178
|
+
title: titleOf(page),
|
|
179
|
+
topic: page.topic
|
|
180
|
+
});
|
|
181
|
+
return yield* log(text);
|
|
182
|
+
});
|
|
183
|
+
const docs = make("docs", {
|
|
184
|
+
json: Boolean("json").pipe(withDefault(false), withDescription$1("Print the topic list, or the page with its topic and title, as one line of JSON")),
|
|
185
|
+
topic: String("topic").pipe(optional, withDescription("The page to print, such as core/jobs or jobs; lists every topic when missing"))
|
|
186
|
+
}, ({ json, topic }) => match$1(topic, {
|
|
187
|
+
onNone: () => listTopics(json),
|
|
188
|
+
onSome: (query) => showTopic(query, json)
|
|
189
|
+
})).pipe(withDescription$2("Print Akter's documentation as Markdown, the pages of apps/docs"));
|
|
190
|
+
export { docs as t };
|
|
@@ -71,7 +71,7 @@ const pages = [
|
|
|
71
71
|
"topic": "deploy/kubernetes"
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
|
-
"markdown": "# Deploying\n\n`akter deploy` bundles an app, uploads it through the Cloud API, confirms that the build is Ready and follows a deployment to `Live`, `NotServing` or `RolledBack`. It uses an Akter API key, not your cloud provider's credentials.\n\n## Sign up and create an app\n\nAkter Cloud's API is at `https://api.akter.dev`, which the CLI uses unless `--url` or `AKTER_CLOUD_URL` names another.\n\n```sh\nnpx
|
|
74
|
+
"markdown": "# Deploying\n\n`akter deploy` bundles an app, uploads it through the Cloud API, confirms that the build is Ready and follows a deployment to `Live`, `NotServing` or `RolledBack`. It uses an Akter API key, not your cloud provider's credentials.\n\n## Sign up and create an app\n\nAkter Cloud's API is at `https://api.akter.dev`, which the CLI uses unless `--url` or `AKTER_CLOUD_URL` names another.\n\n```sh\nnpx @akterjs/cli signup --email you@example.com\nnpx @akterjs/cli organizations create --name acme\nnpx @akterjs/cli apps create --organization acme --name notes\n```\n\n`akter signup` emails you a six-digit code, asks for it, and asks you to accept the [terms of service](../legal/terms.md) and [privacy notice](../legal/privacy.md) once. The session it starts is saved in your OS keychain. An agent without a terminal passes `--code` and `--accept-terms` instead, as [Signing in](../reference/cli.md#signing-in) shows; an agent app can do all of this through the [MCP server](../guides/agents.md) at `https://mcp.akter.dev/mcp` instead.\n\nA new organization runs on the free allowance, $5 of usage a month. `akter billing checkout --organization acme` prints a link to a Stripe page where you add a card; see [Pricing](../pricing.md).\n\n## What a deploy needs\n\nYou need an organization and an app, and a session from `akter login` or an API key with the organization's deploy scope in `AKTER_API_KEY`. A key created with a list of apps reaches only those apps; one without covers every app in the organization. CI and agents use a key: `akter api-keys create` makes one with a name, its scopes (`deploy` for this), a spend cap and an expiry, and prints its secret once.\n\nThe app entry exports `app = { actors, layer }`, the same shape as [local development](../getting-started.md). It must load without starting a listener or running an unbounded program: the CLI builds its layer to inspect contracts and migrations.\n\n## Write the server\n\nYou don't write a separate server entry for the Cloud CLI. Export the actor definitions and their handler layer:\n\n```ts title=\"src/app.layer.ts\"\nimport { Effect, Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect\"\n\nexport const Note = Akter.make(\"Note\", {\n key: Schema.String,\n state: Schema.Struct({ written: Schema.Int }),\n initial: () => ({ written: 0 }),\n commands: {\n write: Akter.command({ payload: { body: Schema.String } }),\n },\n queries: {\n count: Akter.query({ success: Schema.Int }),\n bodies: Akter.query({ success: Schema.Array(Schema.String) }),\n },\n})\n\nexport const app = {\n actors: [Note],\n layer: Note.toLayer({\n migrations: {\n 1: (sql) => sql`CREATE TABLE notes (body TEXT NOT NULL)`,\n 2: (sql) => sql`INSERT INTO notes (body) VALUES (${\"seed\"})`,\n },\n commands: {\n write: (note, { body }) => Effect.gen(function* () {\n const sql = yield* Akter.sql\n yield* sql`INSERT INTO notes (body) VALUES (${body})`\n yield* Note.update({ written: note.written + 1 })\n }),\n },\n queries: {\n count: (note) => Effect.succeed(note.written),\n bodies: () => Effect.gen(function* () {\n const sql = yield* Akter.sql\n const rows = yield* sql<{ body: string }>`SELECT body FROM notes ORDER BY rowid`\n return rows.map((row) => row.body)\n }),\n },\n }),\n}\n```\n\n### Check it\n\nThis small entry demonstrates state and migrations, not a production authorization policy. Define the app's caller kinds and permitted commands using [Access](../core/access.md) before exposing application data.\n\nCheck the entry on real local SQLite before uploading it. Run this script in an empty working folder; it expects the seed row and one write.\n\n```ts title=\"check-app.ts\"\nimport assert from \"node:assert/strict\"\nimport { Effect, Layer } from \"effect\"\nimport { HttpServer } from \"effect/http\"\nimport { Akter } from \"@akterjs/effect\"\nimport { akterClient } from \"@akterjs/client/effect\"\nimport { app, Note } from \"./src/app.layer.ts\"\n\nconst server = Akter.serve({ actors: app.actors, port: 0 }).pipe(\n Layer.provide(Akter.local(\".akter\").pipe(Layer.provide(app.layer))),\n)\nconst client = Layer.unwrap(HttpServer.addressFormattedWith((url) => Effect.succeed(akterClient({ url }))))\n\nconst check = Effect.gen(function* () {\n const note = yield* Note.client(\"lobby\")\n yield* note.write({ body: \"uploaded entry\" })\n assert.equal(yield* note.count(), 1)\n assert.deepEqual(yield* note.bodies(), [\"seed\", \"uploaded entry\"])\n console.log(\"deployment entry checked: state 1; 2 SQL rows\")\n})\n\nEffect.runPromise(check.pipe(Effect.scoped, Effect.provide(client.pipe(Layer.provideMerge(server)))))\n```\n\n```text output\ndeployment entry checked: state 1; 2 SQL rows\n```\n\nThis checks the handlers and numbered SQL migrations. The deploy CLI separately hashes each actor's canonical contract and migrations into its build metadata. It doesn't infer migrations from a database on your laptop.\n\n## Deploy\n\nFrom your app's folder, signed in:\n\n```sh\nnpx @akterjs/cli deploy --organization acme --app notes --entry src/app.layer.ts\n```\n\nIn CI, set `AKTER_API_KEY` through your secret manager instead of signing in. Don't put it in the app entry or command arguments. The organization flag falls back to `AKTER_ORGANIZATION`, then the organization `akter login` saved. The entry defaults to `src/app.layer.ts`; the environment defaults to `prod`, so select the environment explicitly for a preview. See the [CLI reference](../reference/cli.md#akter-deploy) for the full flag list.\n\n## Releases\n\nThe CLI bundles the entry and derives its actor metadata, requests an upload and sends the bytes to the signed URL without its API bearer token. A matching Ready build can be reused without uploading again. It confirms Ready before creating a deployment, then prints the deployment id and its persisted notices once each, in order.\n\nA successful deployment reaches `Live` once every shard has a serving host, prints `Actors at https://<hostname>` and exits 0. The hostname is the environment's own, such as `notes--prod--acme.akter.run`, and an Akter client calls the app's actors there ([Calling actors](../guides/client.md)); a browser opening it gets a 404 that says so. `akter apps get` lists each environment's under `hostnames`, and `akter deploys get` shows a published deploy's `hostname` and `url`. A failed one reaches `RolledBack` and exits nonzero; inspect the failure instead of treating a completed upload as a successful release. A build that was published but had no serving host for every shard within 15 minutes reaches `NotServing`: the CLI prints the reason (for example, no host is up, or the app failed to load) and exits nonzero. It isn't rolled back, because the published build already serves your hostname; deploy a fix to replace it. Build confirmation has a five-minute deadline and deployment following a thirty-minute deadline. A timeout or interrupted CLI does not cancel the remote deployment.\n\nThe current preview's unsupported-owner refusal happens before it changes the live artifact. This isn't a promise that every later publication failure restores a previously published build.\n\n## Storage\n\nThe adopted Cloud design keeps each actor's encrypted SQLite database and WAL directly on the checked-out Archil mount. SQLite owns recovery and checkpointing, and the host waits for durable commit before publishing replies or side effects. A lease or a SQLite lock does not replace the shard's exclusive Archil checkout.\n\nBuild objects are separate from actor state and belong to the provisioned Cloud environment. This CLI uses an Akter API key, not customer storage credentials.\n\n## Where to deploy\n\nThe Cloud CLI is the implemented deployment transport. This page does not provide a supported 1.0 self-hosting recipe for AWS, Google Cloud, Fly.io, Railway, Hetzner, Kubernetes, Docker or Cloudflare Containers. The platform pages state that boundary without unchecked infrastructure code.\n\n`Akter.local` and `akter dev` are the local development path, not a substitute for the production engine's ownership, fencing and host lifecycle.\n\n## Health and shutdown\n\nThe deploy command is finite. It doesn't start your application listener, configure a platform health check, or drain hosts on shutdown. Keep the local [inspector](../guides/operating.md) on loopback; don't expose it as a production operator endpoint.\n\n## Shards\n\nThe Cloud engine owns placement and shard lifecycle. There is no CLI flag here for shard count or region selection.\n\n## Several servers\n\nDon't scale copies of `akter dev` over the same folder to implement a production cluster. Cloud hosts need coordinated shard ownership, fencing and lifecycle management.\n",
|
|
75
75
|
"topic": "deploy/overview"
|
|
76
76
|
},
|
|
77
77
|
{
|
|
@@ -123,7 +123,7 @@ const pages = [
|
|
|
123
123
|
"topic": "pricing"
|
|
124
124
|
},
|
|
125
125
|
{
|
|
126
|
-
"markdown": "# CLI\n\nThe `akter` executable belongs to `@akterjs/cli`. In an app workspace with that package installed, run `bunx akter`. Use `akter --help` and `akter <command> --help` for the flags in your installed build.\n\n| Command | Status |\n| --- | --- |\n| [`akter dev`](#akter-dev) | Local engine and read-only inspector. |\n| [`akter dev snapshots`, `akter dev environments`](#akter-dev-snapshots-and-akter-dev-environments) | Snapshot, branch and promote the local folder's environments, as on Akter Cloud. |\n| [`akter deploy`](#akter-deploy) | Cloud API build upload and deployment progress. |\n| [`akter migrate --all`](#akter-migrate) | Apply migrations to actors in a local folder. |\n| [`akter history`](#akter-history) | Read or verify a local actor's command journal. |\n| [`akter export`](#akter-export) | Download every actor's database and history from Akter Cloud into one `.akter` archive. |\n| [`akter import`](#akter-import) | Read an `.akter` archive into a local folder, checking every entry. |\n| [`akter bench`](#akter-bench) | Run a workload against `akter dev` or an Akter Cloud app and report every outcome. |\n| [`akter <group> <operation>`](#cloud-api-commands) | One command per Cloud API endpoint, such as `akter apps list`. |\n| [`akter login`, `akter signup`, `akter logout`](#signing-in) | Sign in or create an account with an emailed code, save or remove the login every Cloud command uses. |\n| [`akter docs`](#akter-docs) | Print these docs as Markdown. |\n\nOrganization, member, API key, app, build, deploy, domain, log, secret, account, session, usage and billing commands are [Cloud API commands](#cloud-api-commands). Calls to an app's actors are `akter actors call`, `query`, `state`, `watch`, `create`, `ensure` and [`fork`](#forking-an-actor); their files are `akter files` and their computers [`akter computers`](#akter-computers). Locally, [`akter dev snapshots` and `akter dev environments`](#akter-dev-snapshots-and-akter-dev-environments) snapshot, branch and promote. On Cloud, branch, restore and snapshot commands are not registered yet. Older operator commands aren't available either.\n\n## Shared app entry\n\n`dev`, `deploy`, `migrate` and `history` load `--entry`, defaulting to `src/app.layer.ts`. It must export the named value `app = { actors, layer }`. See the [checked deployment entry](../deploy/overview.md#write-the-server) for an example.\n\n## `akter dev`\n\n```sh\nbunx akter dev --entry src/app.layer.ts --dir .akter --port 4000\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--entry` | `src/app.layer.ts` | The module exporting `app`. |\n| `--dir` | `.akter` | The local actor folder. |\n| `--port` | `PORT`, otherwise `4000` | RPC listener port. |\n| `--inspector-port` | RPC port + 1 | Read-only inspector port; a zero RPC port selects a free inspector port. |\n| `--allowed-host` | Loopback names only | Additional allowed Host name; repeatable. |\n| `--reset` | Off | Ask before emptying the local actor folder. |\n| `--from` | None | An `.akter` archive to [import](#akter-import) into `--dir` first; `--dir` must not exist yet. |\n| `--environment` | The promoted one | Serve this environment of the folder, such as a quiet branch, instead of the one promoted to serve. |\n\nThe runner watches the entry for changes and restarts while keeping the folder. RPC is served at `/rpc`. Both listeners bind to loopback and check the Host header; an additional allowed name doesn't authenticate a caller. Don't expose the inspector publicly. See [Operating locally](../guides/operating.md#operator-access).\n\n## `akter dev snapshots` and `akter dev environments`\n\n```sh\nbunx akter dev snapshots create --name before-import\nbunx akter dev environments branch --name trial --snapshot before-import\nbunx akter dev --environment trial --port 4100 # call the quiet branch\nbunx akter dev environments promote --name trial\nbunx akter dev environments promote --name dev # promote back\n```\n\n| Command | Meaning |\n| --- | --- |\n| `snapshots create --name <n>` | Snapshot the serving environment, or `--environment`'s. The `akter dev` serving it takes it; until one does it stays `Pending`. |\n| `snapshots list`, `snapshots get --name <n>` | The environment's live snapshots. Ids are `snapshot-<n>`, numbered from 0 and never reused. |\n| `snapshots delete --name <n>` | Retire the snapshot and remove its copy. Its name is free again; its id isn't. |\n| `environments list`, `environments get --name <e>` | Every environment, deleted ones included, with `serving` and `quiet`. |\n| `environments branch --name <e> --snapshot <n>` | A new, quiet environment from a `Ready` snapshot of the serving environment, or `--from`'s. |\n| `environments promote --name <e>` | Serve `<e>` instead and lift its quiet. Promoting the one that serves changes nothing and prints the same result. |\n| `environments delete --name <e>` | Remove an environment that doesn't serve. `dev` and the serving one are refused; the name stays taken. |\n\nEvery command takes `--dir` and `--json`; with `--json` it prints the same shapes as Akter Cloud's `snapshots` and `environments` operations, and a refusal prints `code`, `reason` and `message`. The promote prints what it leaves out. See [Snapshots, branches and promote](../guides/operating.md#snapshots-branches-and-promote).\n\n## `akter deploy`\n\n```sh\nnpx akter deploy \\\n --organization \"$AKTER_ORGANIZATION\" \\\n --app \"$AKTER_APP\" \\\n --environment \"$AKTER_ENVIRONMENT\"\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--organization` | `AKTER_ORGANIZATION` | Existing organization. |\n| `--app` | Required | Existing app. |\n| `--environment` | `prod` | Provisioned destination environment. |\n| `--url` | `AKTER_CLOUD_URL`, then the saved login, then `https://api.akter.dev` | Cloud API base URL; HTTPS except for loopback development. |\n| `--entry` | `src/app.layer.ts` | The module exporting `app`. |\n| `--domain` | None | Claim this customer hostname for the environment and print the CNAME to add. |\n| `--json` | Off | Print the live deploy as one line of JSON in the API's `Deploy` schema, with its `hostname` and `url`; progress goes to stderr. |\n\nThe key comes from [signing in](#signing-in) and needs deploy scope for the organization. The key is exchanged for a short-lived bearer token; signed uploads don't receive that bearer. The CLI rejects redirects for token exchange, API requests and uploads.\n\nThe CLI uploads a new build or reuses a matching Ready one, confirms Ready, creates a new deployment and prints its id. Persisted notices are printed once each in order until `Live`, `NotServing` or `RolledBack`. `Live` prints `Actors at https://<hostname>`, the app's own hostname for that environment, where an Akter client calls its actors, and exits 0; `NotServing` prints why the published build isn't serving and exits nonzero; `RolledBack` exits nonzero. Build confirmation and deployment following have five-minute and thirty-minute deadlines respectively. Interrupting or timing out the CLI doesn't cancel the remote deployment.\n\n`akter deploy` doesn't create the organization or the app: `akter organizations create` and `akter apps create` do. An upload that succeeds doesn't by itself mean the deployment goes live; wait for `Live`.\n\nThere is no region flag. See [Deploying](../deploy/overview.md).\n\n## `akter migrate`\n\n```sh\nbunx akter migrate --all --dry-run --entry src/app.layer.ts --dir .akter\n```\n\n`--all` is required; migrating one actor from the CLI isn't implemented. Without `--dry-run`, pending migrations are applied now rather than on the actor's next open. Output names each actor's old and new migration version. A failure stops the command, so actors later in the traversal aren't migrated by that run. This command operates on a local folder, not the Cloud API.\n\n## `akter history`\n\n```sh\nbunx akter history Note/lobby --entry src/app.layer.ts --dir .akter --json\nbunx akter history Note/lobby --entry src/app.layer.ts --dir .akter --verify\n```\n\nThe journal is read oldest first. `--json` emits one JSON object per command. `--since` accepts a whole number with `ms`, `s`, `m`, `h` or `d`; a retained-history boundary is printed when the journal doesn't cover the full history.\n\n`--verify` replays the kept history against the current handlers and reports either no difference or the first mismatching outcome. It can't be combined with `--since`, and it refuses a replay whose initial history has been pruned. To verify a Cloud app's actors, export the app, import the archive and run `--verify` on the folder ([Use the exported data](../guides/exporting.md#use-the-exported-data)). See [History and replay](../core/history-and-replay.md).\n\n## `akter export`\n\n```sh\nbunx akter export \\\n --organization \"$AKTER_ORGANIZATION\" \\\n --app \"$AKTER_APP\" \\\n --url \"$AKTER_CLOUD_URL\" \\\n --out ./chat.akter\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--organization` | `AKTER_ORGANIZATION` | Existing organization. |\n| `--app` | Required | The app whose actors to export. |\n| `--url` | `AKTER_CLOUD_URL`, then the saved login, then `https://api.akter.dev` | Cloud API base URL; HTTPS except for loopback development. |\n| `--out` | Required | The `.akter` archive to write; it must not exist yet. Run again with the same path to resume. |\n\nThe key comes from [signing in](#signing-in) and needs export scope for the organization; a key with only deploy, call or read scope is refused. Every listing, snapshot and download request is authorized for that organization's app, and none of them reach another app or organization.\n\nAn export is one `.akter` file: a tar archive compressed with zstd (ADR 0019). Its first entry is `manifest.json`, which records the format version (1), when it was written, the size and SHA-256 of every other entry, the `computers` section with each computer the app's actors keep (its disk and checkpoint stay in Akter Cloud, so the checkpoint is marked `\"portable\": false`), and the `lineage` section, with one entry per forked actor: the actor, the actor it was forked from (`parent`), the fork's id (`fork`) and the parent's commit version at the cut (`forkedAt`). The entries after it are:\n\n- `shards/<shard>/<Actor>/<key>/actor.db` for each actor that has a database, in the layout `akter dev` uses, so `bunx akter dev --from ./chat.akter --dir ./chat` serves the exported actors.\n- Each `actor.db` is a plain, decrypted SQLite database that stock `sqlite3` opens. It holds the actor's whole database at one consistent point after its last committed command: its state, its own tables, its command journal (the history `akter history` reads), and its pending timers, outbox and jobs.\n- `shards/<shard>/<Actor>/<key>/files/<sha256>` for each distinct file the actor's database names, its plaintext content named by its SHA-256. The paths, sizes and times of the actor's files stay in its `actor.db`.\n- Nothing else. It holds no Cloud keys, no export records and no file keys: the CLI blanks every file grant in `actor.db` and rewrites the file before it archives it.\n\nAn erased actor has nothing to export: the CLI prints `<Actor>/<key>: erased, nothing to export` and goes on.\n\nEach actor is snapshotted on its own, after a commit and while no command runs on it, so its database is consistent. Actors are exported one after another, so two actors' snapshots come from different moments. The snapshot stays encrypted on the server under a key of its own, and only the download decrypts it. When the snapshot is taken, its completion record gets a MAC over every page in order. The server checks that MAC in the same pass that computes the snapshot's SHA-256, before it reports the snapshot, so a page rolled back to an earlier copy of itself stops the export with a gone-snapshot error instead of exporting stale data. That check covers the snapshot file only. The live `actor.db` on the server is authenticated page by page, so a page rolled back inside it is outside what export can detect, and a snapshot taken afterwards exports the rolled-back page as it is. Starting an actor's export replaces that actor's previous snapshot, so run one export of an app at a time.\n\nThe CLI downloads into a staging folder beside the archive, `<out>.staging`, at most 64 KiB per request, and prints each actor's progress every MiB and when it's done. It downloads an actor's files first, each into `files/<sha256>.export` and checked against its SHA-256 before it's renamed, then its database, which it checks against the SHA-256 the server computed when it took the snapshot, strips of file grants and only then renames to `actor.db`. While an actor downloads, `actor.db.export` holds the database's bytes so far and `actor.db.export.json` the snapshot they come from. A finished file is kept for the next run, and a file the actor's snapshot doesn't hold is removed from `files`. After an interruption, run the same command with the same `--out`: finished actors are kept, and a partly downloaded actor continues from the same snapshot. When every actor is in, the CLI writes the archive as `<out>.partial`, renames it to `<out>`, removes the staging folder and prints `Wrote <out> with <n> entries`. An export is complete only when it prints that line. If that snapshot was replaced, failed authentication on the server or doesn't match its SHA-256, the CLI deletes the partial download and exits nonzero; run it again to take a new snapshot.\n\nSee [Exporting an app](../guides/exporting.md) for a walkthrough.\n\n## `akter import`\n\n```sh\nbunx akter import ./chat.akter --dir ./chat\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `<archive>` | Required | The `.akter` archive to read. |\n| `--dir` | `.akter` | A new folder to read the actors into; it must not exist yet. |\n\nThe CLI reads the archive as a stream into `<dir>.importing`, checks every entry against the size and SHA-256 its manifest records, takes each actor's `files/<sha256>` into the folder's file home, `files-home`, under the actor's local identity, and renames the folder to `--dir` only once every entry has passed and every file is in. It refuses an archive whose first entry isn't `manifest.json`, that holds an entry its manifest doesn't list, whose entry doesn't match its checksum, that was cut short, that a newer akter wrote, or whose files can't be taken in, and then leaves no folder behind. Every format version akter has written stays readable. `akter dev`, `akter history` and `akter migrate` read the imported folder.\n\n## `akter bench`\n\n```sh\nbunx akter bench tests/sale.workload.ts --duration 30s --concurrency 8\nbunx akter bench tests/doc.workload.ts --duration 60s --json\n```\n\nThe workload module exports `workload = ({ id, kind, url }) => ({ name, open, step })`. `id` names this run, `url` is the target and `kind` says what it is: `\"local\"` for `akter dev`, `\"cloud\"` for an Akter Cloud app and `\"comparison\"` for another system, so a workload sends a development identity such as the `akter-principal` header only when `kind` is `\"local\"`. `open` is an Effect that needs an Akter client and returns a session, and `step(session)` is one operation whose success value names its outcome. Without `--url`, the command starts `akter dev` with `--entry` on an empty temporary folder and stops it when the run ends. A loopback `--url` reuses a running `akter dev`. Any other `--url` is an Akter Cloud app: the command signs in with `AKTER_API_KEY` or the key `akter login` saved, and keeps the run signed in past the 15-minute token. `--system <name> --url <url>` runs a workload against another system for comparison and signs in to nothing.\n\n`--concurrency` workers (8 by default) each run one step at a time and start no new step after `--duration` (30s by default). A step, or the workload's `open`, with no answer within `--step-timeout` (30s by default) counts as `failed: no answer within <ms> ms` and the worker moves on, so one stuck step can't hold the run open. The report has one row per outcome, and a failed step counts as `failed: <error tag>`, so failures and losses stay in the table. Each row and the total give steps, steps per second over the whole run, and p50, p90, p99 and max latency in milliseconds. Cost per row is always `unmeasured`.\n\nA local run's output starts with a `LOCAL RUN` notice: the numbers describe this machine and this checkout, not Akter Cloud. A Cloud run's starts with `AKTER CLOUD RUN`: every latency includes the network path from the machine that ran the bench to the app. The report also names the hardware, the runtime, the versions of `@akterjs/cli`, `@akterjs/client`, `@akterjs/effect` and `effect`, the git commit and whether the worktree is clean, and the SHA-256 of the nearest lockfile. `--json` prints the same report as one JSON object, with `\"local\": true` for a local run and `false` for a Cloud run.\n\n## Operator commands\n\nThe old `inspect`, `log`, `snapshot` and `retention` command groups aren't registered. Local inspection is the read-only listener started by `dev`; see [Operating locally](../guides/operating.md).\n\n## Forking an actor\n\n`akter actors fork --url <app url> --actor <Actor> --key <key> --into <new key>` copies an actor into a new actor at another key: its state and files at one moment, with none of its work in flight, since its jobs, timers and undelivered messages stay with the original. It prints the original's commit version at the cut. A retry with the same `--command-id` makes one copy, and `--dry-run` prints the request without sending it. There is no top-level `akter fork`; don't use the old history-copying examples against a 1.0 actor folder.\n\nOn the local engine the copy also gets each of the actor's computers: its disk as it was at the cut, stopped, so no process the original ran runs in the copy. While it copies them the fork holds the original's computers paused under the fork's id: a step already running finishes first, and one sent meanwhile fails with `ComputerHeldError` naming the fork's id. Then each computer goes back to where it was. If the engine stops partway through a fork, the hold stays until a retry with the same id finishes the fork. A fork whose copies would pass the store's limit of 4 computers is refused with `InvalidRequestError`. Akter Cloud copies each computer by its disk alone too, but to do that it stops the original's computer first, so one that was running starts again from its disk with its memory and processes gone, and one that was paused stays stopped. A copy that fails keeps the hold until a retry with the same fork id copies it. Akter Cloud refuses, with `ActorUnavailableError`, a fork whose copies would pass the environment's limit of 4 computers, and a fork with computers into a key on another shard.\n\nWhere the fork runs depends on the target, the same rule every actor command and `akter bench` use. A loopback `--url` (or `AKTER_APP_URL`), such as the address `akter dev` prints, is the local engine: the command sends the actor's fork RPC, `<Actor>/fork`, with no sign-in and no question. Any other target is Akter Cloud, where forking is an operator action (decision 5): the gateway refuses the fork RPC for every caller, so the command runs the Cloud API operation `actorForks.create` instead, `POST /v1/organizations/{organization}/apps/{app}/environments/{environment}/actors/{actor}/{key}/forks` with `into` and `forkId`:\n\n```sh\nakter actors fork --organization acme --app shop --environment prod --actor Sale --key s1 --into s1-try\n```\n\nIt signs in with your credentials (`AKTER_API_KEY` or `akter login`) at the Cloud API (`AKTER_CLOUD_URL` or the URL `akter login` saved); `--organization` defaults as for every Cloud command, and `--command-id` is sent as the fork id. Like every dangerous Cloud command it asks the person at the terminal to confirm, and exits 4 when no terminal can ask. An API key's fork also needs its creator's approval: at a terminal the command asks for the emailed code and retries once with the token, and without one it takes `--approval <token>` from `akter approvals request` and `akter approvals confirm`. The key needs the `fork` scope. On the hosted MCP server the same fork is the `actors_fork` tool, which takes `organization`, `app`, `environment`, `actor`, `key`, `into` and `forkId` and asks the person before anything is sent. The Effect and Promise clients' `fork(into)` stays an actor RPC, so it forks on `akter dev` and is refused on Akter Cloud by design.\n\nCloud forks are a preview behind `cloudFork`, default off. The CLI leaves `actors fork` out of help but still accepts it by name; control refuses an organization without the flag. MCP currently hides `actors_fork` from everyone until it can read organization flags, without falling back to the forbidden actor RPC. Local forks do not use a flag.\n\n## Cloud API commands\n\nEvery Cloud API endpoint is one command, `akter <group> <operation>`, named after the API's own groups and operations: `akter apps list`, `akter apps create`, `akter deploys get`, `akter members change-role` and so on. `akter --help` lists the groups and `akter <group> --help` their operations. The same list, with the same names, descriptions, input schemas and safety hints, is what Akter's MCP tools are built from, so a command and its tool always agree.\n\n- Each field of the operation's input is a flag, such as `--organization` and `--name`. `--input '<json>'` takes the whole input as one JSON object, the same arguments the MCP tool takes; `--input @file.json` reads a file and `--input -` reads stdin. Flags override its fields. Pass a secret, such as a `secrets set` value, with `--input -` or `--input @file.json` rather than a flag, so it stays out of the process list and shell history; `--dry-run` prints the body, secret included. The CLI checks the input against the operation's schema before it sends anything and exits 2 when it doesn't match.\n- `--json` prints the API's answer as one line of JSON in the API's own schema, after checking it against that schema; an answer that doesn't match exits 1, because the CLI and the API disagree. Without `--json` the answer is printed indented. A refusal prints the API's error body with `--json`, and one line on stderr otherwise. After a 503 on a change that is safe to repeat, such as `akter actors erase`, that line adds that it may have stopped partway and to run the same command again.\n- A read that pages takes `--after` and answers with `next` while there is more: `akter actors list`, which lists an app's actors by name and key (`--actor Room` for one name, `--environment` for an environment other than `prod`), and `akter actors history`. An `akter actors list` page holds `--limit` actors (500 at most, and by default) unless it is the last, and the last page has no `next`, so read on until `next` is absent. `--all` does that: it sends each page's `next` as `--after` until a page has none and prints one answer in the API's schema with every page's entries and no `next`. A refused page stops it with that page's error and exit code.\n- Commands that change something take `--dry-run`, which checks the input and prints the request instead of sending it.\n- Destructive commands, such as `akter apps delete`, ask a person to confirm. When stdin or stdout isn't a terminal they never ask: they exit 4 at once, unless `--approval <token>` carries an approval for the action.\n- Signed in with an API key, a dangerous action also needs its creator's approval, and the API refuses it with 428 `ConfirmationRequiredError` until a token comes with it. At a terminal the CLI asks the API to email the key's creator a code, asks you to type it, turns it into a single-use token and sends the command once more; it never prints the token. A refused code exits 3. Without a terminal it exits 4 with reason `ApprovalRequired` and names the steps: `akter approvals request` emails the code, `akter approvals confirm` turns it into a token, and the command runs again with `--approval <token>`.\n- `--organization` defaults to `AKTER_ORGANIZATION`, then the organization saved by `akter login`; `--url` to `AKTER_CLOUD_URL`, then the saved URL, then `https://api.akter.dev`.\n\n## `akter computers`\n\n`akter computers <list|get|logs|pause|resume|stop|delete> --url <app url> --actor <Actor> --key <key>` reaches one actor's computers over its `<Actor>/computer/<op>` RPCs, and each operation is also the `computers_<op>` MCP tool. Every operation but `list` takes `--computer <name>`. `logs` prints a page of the computer's log, oldest first, each line with its `seq`; `--after <seq>` reads on after the last line printed and `--limit` takes 1 to 1000 lines. Each computer's lines also reach the app's log with source `computer` and subject `<environment>/<Actor>/<key>/<computer>`, so `akter logs follow --app <app> --source computer` follows every computer of the app at once. `pause` keeps memory and disk, `stop` keeps the disk, and `resume` fails while a fork holds the computer. There is no command to make a computer or run one of its commands: only the actor's own jobs do that.\n\n`delete` removes the computer and its disk, so it asks a person to confirm, exits 4 when stdin or stdout isn't a terminal, and prints the request without asking under `--dry-run`.\n\n## `akter logs follow`\n\n`akter logs follow --app <app>` prints an app's log lines as they arrive, oldest first, one per line: when, the level, what wrote it and what it's about, then the message. It keeps reading after the last line it printed and waits two seconds when nothing new has come. It runs until interrupted, or until `--count <n>` lines have printed. `--deploy`, `--since` and `--source` narrow it as they do `akter logs read`; the sources are `deploy`, a deploy's progress with its id as the subject, and `computer`, what happened to one of the app's computers. `--json` prints each line as one line of JSON in the API's `LogLine` schema.\n\nIt never prompts. A timeout, a dropped connection, 429 or 5xx is retried with backoff, and each retry is noted on stderr. Any other refusal ends it with the exit code below, such as 1 for an app that doesn't exist and 3 for a key without access.\n\n## Signing in\n\nEvery Cloud command, `deploy` and `export` included, signs in the same way:\n\n1. `AKTER_API_KEY`, an API key or an agent key, when it is set and not empty. Scripts, CI and agents use this. When `AKTER_CLOUD_URL` is set too, the key goes only to that origin: a `--url` naming another server exits 2 with `ApiKeyForOtherUrl`, naming both origins, instead of sending the key there. Drop `--url`, or point `AKTER_CLOUD_URL` at the server you mean.\n2. Otherwise the session or key `akter login` saved. It goes only to the Cloud URL it was saved for: a `--url` or `AKTER_CLOUD_URL` that names another server exits 3 with `LoginForOtherUrl` instead of sending it there. Sign in for that URL, or set `AKTER_API_KEY`.\n\n`akter login --email <address>` emails a six-digit code and asks for it when a person is at the terminal; `akter signup` is the same, for an address with no account yet, since the first sign-in creates it. Without a terminal, as an agent runs it, the command emails the code, prints how to finish (with `--json`, `{ \"codeSent\": true, \"email\", \"expiresAt\" }`) and exits 0 without waiting; `akter login --email <address> --code <code>` then finishes. A refused code exits 3 and saves nothing; `--email` missing without a terminal exits 2. A code that creates an account first needs Akter's terms of service and privacy notice accepted, which `akter docs legal/terms` and `akter docs legal/privacy` print. The CLI asks a person at the terminal once, naming their version, and then sends the same code again; `--accept-terms` accepts them without asking, for an agent. Without a terminal or the flag, it exits 4, names the flag and saves nothing, and the code still works for the next run. A sign-in to an existing account is never asked. The session's secret is saved like a key, below, and never printed: `--json` prints the account, whether this sign-in `created` it, the session's id and expiry, and where it was saved.\n\n`akter login --with-api-key --url <cloud-url>` saves a key instead: it reads the key from stdin or a hidden prompt, never from a flag, checks it with the Cloud API and saves it. The key goes to the OS keychain (macOS Keychain, or the Secret Service through `secret-tool` on Linux) when one is reachable, and otherwise into `$XDG_CONFIG_HOME/akter/login.json` (`~/.config/akter/login.json`), which only you can read (mode 0600). `akter logout` removes it.\n\nWithout either, a Cloud command exits 3.\n\n## `akter docs`\n\n`akter docs` lists the topics of these docs, and `akter docs <topic>` prints one page as Markdown, such as `akter docs reference/cli`, or `akter docs cli` when only one page has that name. `--json` prints the list, or the page with its topic and title, as one line of JSON. An unknown topic exits 2 and lists the topics that contain it.\n\n## Exit codes\n\n| Code | Meaning |\n| --- | --- |\n| 0 | Done. |\n| 1 | Failed: the API refused (404, 409 and the like), the answer broke its schema, or a command such as `deploy` ended without success. |\n| 2 | Usage: an unknown flag, a missing or invalid input, or a 400 or 422 from the API. |\n| 3 | Authentication: not signed in, the API answered 401 or 403, including when it refused the key at sign-in, or an actor refused the caller, by its kind (`CallerNotAllowedError`) or by its `files.authorize` rule (`FileAccessDeniedError`). |\n| 4 | Confirmation: a person had to confirm or approve and couldn't be asked, or declined; also a 428 from the API. |\n| 5 | Unavailable: the API couldn't be reached, timed out, or answered 408, 429 or 5xx, including at sign-in. Retrying may help. Every Cloud command's sign-in first retries a 5xx for about 30 seconds, so it rides out a Cloud deploy. |\n| 130 | Interrupted. |\n\n`deploy` and `export` use the same codes when the API answers with a status, and 1 when it can't be reached; signing in, their first step, exits 3 or 5 as above. Treat a nonzero exit as failure and retain its diagnostic. With `--json`, a failure the CLI detects itself prints one JSON object with `code`, `reason` and `message`. Help exits successfully, deployment success requires the terminal `Live` notice, and an export is complete only when it prints `Exported <n> actors to <folder>` and exits 0. An uploaded build, a printed deployment id or an interrupted follow isn't equivalent to a successful deployment.\n",
|
|
126
|
+
"markdown": "# CLI\n\nThe `akter` executable belongs to `@akterjs/cli`. In an app workspace with that package installed, run `bunx akter`. Use `akter --help` and `akter <command> --help` for the flags in your installed build.\n\n| Command | Status |\n| --- | --- |\n| [`akter dev`](#akter-dev) | Local engine and read-only inspector. |\n| [`akter dev snapshots`, `akter dev environments`](#akter-dev-snapshots-and-akter-dev-environments) | Snapshot, branch and promote the local folder's environments, as on Akter Cloud. |\n| [`akter deploy`](#akter-deploy) | Cloud API build upload and deployment progress. |\n| [`akter migrate --all`](#akter-migrate) | Apply migrations to actors in a local folder. |\n| [`akter history`](#akter-history) | Read or verify a local actor's command journal. |\n| [`akter export`](#akter-export) | Download every actor's database and history from Akter Cloud into one `.akter` archive. |\n| [`akter import`](#akter-import) | Read an `.akter` archive into a local folder, checking every entry. |\n| [`akter bench`](#akter-bench) | Run a workload against `akter dev` or an Akter Cloud app and report every outcome. |\n| [`akter <group> <operation>`](#cloud-api-commands) | One command per Cloud API endpoint, such as `akter apps list`. |\n| [`akter login`, `akter signup`, `akter logout`](#signing-in) | Sign in or create an account with an emailed code, save or remove the login every Cloud command uses. |\n| [`akter docs`](#akter-docs) | Print these docs as Markdown. |\n\nOrganization, member, API key, app, build, deploy, domain, log, secret, account, session, usage and billing commands are [Cloud API commands](#cloud-api-commands). Calls to an app's actors are `akter actors call`, `query`, `state`, `watch`, `create`, `ensure` and [`fork`](#forking-an-actor); their files are `akter files` and their computers [`akter computers`](#akter-computers). Locally, [`akter dev snapshots` and `akter dev environments`](#akter-dev-snapshots-and-akter-dev-environments) snapshot, branch and promote. On Cloud, branch, restore and snapshot commands are not registered yet. Older operator commands aren't available either.\n\n## Shared app entry\n\n`dev`, `deploy`, `migrate` and `history` load `--entry`, defaulting to `src/app.layer.ts`. It must export the named value `app = { actors, layer }`. See the [checked deployment entry](../deploy/overview.md#write-the-server) for an example.\n\n## `akter dev`\n\n```sh\nbunx akter dev --entry src/app.layer.ts --dir .akter --port 4000\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--entry` | `src/app.layer.ts` | The module exporting `app`. |\n| `--dir` | `.akter` | The local actor folder. |\n| `--port` | `PORT`, otherwise `4000` | RPC listener port. |\n| `--inspector-port` | RPC port + 1 | Read-only inspector port; a zero RPC port selects a free inspector port. |\n| `--allowed-host` | Loopback names only | Additional allowed Host name; repeatable. |\n| `--reset` | Off | Ask before emptying the local actor folder. |\n| `--from` | None | An `.akter` archive to [import](#akter-import) into `--dir` first; `--dir` must not exist yet. |\n| `--environment` | The promoted one | Serve this environment of the folder, such as a quiet branch, instead of the one promoted to serve. |\n\nThe runner watches the entry for changes and restarts while keeping the folder. RPC is served at `/rpc`. Both listeners bind to loopback and check the Host header; an additional allowed name doesn't authenticate a caller. Don't expose the inspector publicly. See [Operating locally](../guides/operating.md#operator-access).\n\n## `akter dev snapshots` and `akter dev environments`\n\n```sh\nbunx akter dev snapshots create --name before-import\nbunx akter dev environments branch --name trial --snapshot before-import\nbunx akter dev --environment trial --port 4100 # call the quiet branch\nbunx akter dev environments promote --name trial\nbunx akter dev environments promote --name dev # promote back\n```\n\n| Command | Meaning |\n| --- | --- |\n| `snapshots create --name <n>` | Snapshot the serving environment, or `--environment`'s. The `akter dev` serving it takes it; until one does it stays `Pending`. |\n| `snapshots list`, `snapshots get --name <n>` | The environment's live snapshots. Ids are `snapshot-<n>`, numbered from 0 and never reused. |\n| `snapshots delete --name <n>` | Retire the snapshot and remove its copy. Its name is free again; its id isn't. |\n| `environments list`, `environments get --name <e>` | Every environment, deleted ones included, with `serving` and `quiet`. |\n| `environments branch --name <e> --snapshot <n>` | A new, quiet environment from a `Ready` snapshot of the serving environment, or `--from`'s. |\n| `environments promote --name <e>` | Serve `<e>` instead and lift its quiet. Promoting the one that serves changes nothing and prints the same result. |\n| `environments delete --name <e>` | Remove an environment that doesn't serve. `dev` and the serving one are refused; the name stays taken. |\n\nEvery command takes `--dir` and `--json`; with `--json` it prints the same shapes as Akter Cloud's `snapshots` and `environments` operations, and a refusal prints `code`, `reason` and `message`. The promote prints what it leaves out. See [Snapshots, branches and promote](../guides/operating.md#snapshots-branches-and-promote).\n\n## `akter deploy`\n\n```sh\nnpx @akterjs/cli deploy \\\n --organization \"$AKTER_ORGANIZATION\" \\\n --app \"$AKTER_APP\" \\\n --environment \"$AKTER_ENVIRONMENT\"\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--organization` | `AKTER_ORGANIZATION` | Existing organization. |\n| `--app` | Required | Existing app. |\n| `--environment` | `prod` | Provisioned destination environment. |\n| `--url` | `AKTER_CLOUD_URL`, then the saved login, then `https://api.akter.dev` | Cloud API base URL; HTTPS except for loopback development. |\n| `--entry` | `src/app.layer.ts` | The module exporting `app`. |\n| `--domain` | None | Claim this customer hostname for the environment and print the CNAME to add. |\n| `--json` | Off | Print the live deploy as one line of JSON in the API's `Deploy` schema, with its `hostname` and `url`; progress goes to stderr. |\n\nThe key comes from [signing in](#signing-in) and needs deploy scope for the organization. The key is exchanged for a short-lived bearer token; signed uploads don't receive that bearer. The CLI rejects redirects for token exchange, API requests and uploads.\n\nThe CLI uploads a new build or reuses a matching Ready one, confirms Ready, creates a new deployment and prints its id. Persisted notices are printed once each in order until `Live`, `NotServing` or `RolledBack`. `Live` prints `Actors at https://<hostname>`, the app's own hostname for that environment, where an Akter client calls its actors, and exits 0; `NotServing` prints why the published build isn't serving and exits nonzero; `RolledBack` exits nonzero. Build confirmation and deployment following have five-minute and thirty-minute deadlines respectively. Interrupting or timing out the CLI doesn't cancel the remote deployment.\n\n`akter deploy` doesn't create the organization or the app: `akter organizations create` and `akter apps create` do. An upload that succeeds doesn't by itself mean the deployment goes live; wait for `Live`.\n\nThere is no region flag. See [Deploying](../deploy/overview.md).\n\n## `akter migrate`\n\n```sh\nbunx akter migrate --all --dry-run --entry src/app.layer.ts --dir .akter\n```\n\n`--all` is required; migrating one actor from the CLI isn't implemented. Without `--dry-run`, pending migrations are applied now rather than on the actor's next open. Output names each actor's old and new migration version. A failure stops the command, so actors later in the traversal aren't migrated by that run. This command operates on a local folder, not the Cloud API.\n\n## `akter history`\n\n```sh\nbunx akter history Note/lobby --entry src/app.layer.ts --dir .akter --json\nbunx akter history Note/lobby --entry src/app.layer.ts --dir .akter --verify\n```\n\nThe journal is read oldest first. `--json` emits one JSON object per command. `--since` accepts a whole number with `ms`, `s`, `m`, `h` or `d`; a retained-history boundary is printed when the journal doesn't cover the full history.\n\n`--verify` replays the kept history against the current handlers and reports either no difference or the first mismatching outcome. It can't be combined with `--since`, and it refuses a replay whose initial history has been pruned. To verify a Cloud app's actors, export the app, import the archive and run `--verify` on the folder ([Use the exported data](../guides/exporting.md#use-the-exported-data)). See [History and replay](../core/history-and-replay.md).\n\n## `akter export`\n\n```sh\nbunx akter export \\\n --organization \"$AKTER_ORGANIZATION\" \\\n --app \"$AKTER_APP\" \\\n --url \"$AKTER_CLOUD_URL\" \\\n --out ./chat.akter\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--organization` | `AKTER_ORGANIZATION` | Existing organization. |\n| `--app` | Required | The app whose actors to export. |\n| `--url` | `AKTER_CLOUD_URL`, then the saved login, then `https://api.akter.dev` | Cloud API base URL; HTTPS except for loopback development. |\n| `--out` | Required | The `.akter` archive to write; it must not exist yet. Run again with the same path to resume. |\n\nThe key comes from [signing in](#signing-in) and needs export scope for the organization; a key with only deploy, call or read scope is refused. Every listing, snapshot and download request is authorized for that organization's app, and none of them reach another app or organization.\n\nAn export is one `.akter` file: a tar archive compressed with zstd (ADR 0019). Its first entry is `manifest.json`, which records the format version (1), when it was written, the size and SHA-256 of every other entry, the `computers` section with each computer the app's actors keep (its disk and checkpoint stay in Akter Cloud, so the checkpoint is marked `\"portable\": false`), and the `lineage` section, with one entry per forked actor: the actor, the actor it was forked from (`parent`), the fork's id (`fork`) and the parent's commit version at the cut (`forkedAt`). The entries after it are:\n\n- `shards/<shard>/<Actor>/<key>/actor.db` for each actor that has a database, in the layout `akter dev` uses, so `bunx akter dev --from ./chat.akter --dir ./chat` serves the exported actors.\n- Each `actor.db` is a plain, decrypted SQLite database that stock `sqlite3` opens. It holds the actor's whole database at one consistent point after its last committed command: its state, its own tables, its command journal (the history `akter history` reads), and its pending timers, outbox and jobs.\n- `shards/<shard>/<Actor>/<key>/files/<sha256>` for each distinct file the actor's database names, its plaintext content named by its SHA-256. The paths, sizes and times of the actor's files stay in its `actor.db`.\n- Nothing else. It holds no Cloud keys, no export records and no file keys: the CLI blanks every file grant in `actor.db` and rewrites the file before it archives it.\n\nAn erased actor has nothing to export: the CLI prints `<Actor>/<key>: erased, nothing to export` and goes on.\n\nEach actor is snapshotted on its own, after a commit and while no command runs on it, so its database is consistent. Actors are exported one after another, so two actors' snapshots come from different moments. The snapshot stays encrypted on the server under a key of its own, and only the download decrypts it. When the snapshot is taken, its completion record gets a MAC over every page in order. The server checks that MAC in the same pass that computes the snapshot's SHA-256, before it reports the snapshot, so a page rolled back to an earlier copy of itself stops the export with a gone-snapshot error instead of exporting stale data. That check covers the snapshot file only. The live `actor.db` on the server is authenticated page by page, so a page rolled back inside it is outside what export can detect, and a snapshot taken afterwards exports the rolled-back page as it is. Starting an actor's export replaces that actor's previous snapshot, so run one export of an app at a time.\n\nThe CLI downloads into a staging folder beside the archive, `<out>.staging`, at most 64 KiB per request, and prints each actor's progress every MiB and when it's done. It downloads an actor's files first, each into `files/<sha256>.export` and checked against its SHA-256 before it's renamed, then its database, which it checks against the SHA-256 the server computed when it took the snapshot, strips of file grants and only then renames to `actor.db`. While an actor downloads, `actor.db.export` holds the database's bytes so far and `actor.db.export.json` the snapshot they come from. A finished file is kept for the next run, and a file the actor's snapshot doesn't hold is removed from `files`. After an interruption, run the same command with the same `--out`: finished actors are kept, and a partly downloaded actor continues from the same snapshot. When every actor is in, the CLI writes the archive as `<out>.partial`, renames it to `<out>`, removes the staging folder and prints `Wrote <out> with <n> entries`. An export is complete only when it prints that line. If that snapshot was replaced, failed authentication on the server or doesn't match its SHA-256, the CLI deletes the partial download and exits nonzero; run it again to take a new snapshot.\n\nSee [Exporting an app](../guides/exporting.md) for a walkthrough.\n\n## `akter import`\n\n```sh\nbunx akter import ./chat.akter --dir ./chat\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `<archive>` | Required | The `.akter` archive to read. |\n| `--dir` | `.akter` | A new folder to read the actors into; it must not exist yet. |\n\nThe CLI reads the archive as a stream into `<dir>.importing`, checks every entry against the size and SHA-256 its manifest records, takes each actor's `files/<sha256>` into the folder's file home, `files-home`, under the actor's local identity, and renames the folder to `--dir` only once every entry has passed and every file is in. It refuses an archive whose first entry isn't `manifest.json`, that holds an entry its manifest doesn't list, whose entry doesn't match its checksum, that was cut short, that a newer akter wrote, or whose files can't be taken in, and then leaves no folder behind. Every format version akter has written stays readable. `akter dev`, `akter history` and `akter migrate` read the imported folder.\n\n## `akter bench`\n\n```sh\nbunx akter bench tests/sale.workload.ts --duration 30s --concurrency 8\nbunx akter bench tests/doc.workload.ts --duration 60s --json\n```\n\nThe workload module exports `workload = ({ id, kind, url }) => ({ name, open, step })`. `id` names this run, `url` is the target and `kind` says what it is: `\"local\"` for `akter dev`, `\"cloud\"` for an Akter Cloud app and `\"comparison\"` for another system, so a workload sends a development identity such as the `akter-principal` header only when `kind` is `\"local\"`. `open` is an Effect that needs an Akter client and returns a session, and `step(session)` is one operation whose success value names its outcome. Without `--url`, the command starts `akter dev` with `--entry` on an empty temporary folder and stops it when the run ends. A loopback `--url` reuses a running `akter dev`. Any other `--url` is an Akter Cloud app: the command signs in with `AKTER_API_KEY` or the key `akter login` saved, and keeps the run signed in past the 15-minute token. `--system <name> --url <url>` runs a workload against another system for comparison and signs in to nothing.\n\n`--concurrency` workers (8 by default) each run one step at a time and start no new step after `--duration` (30s by default). A step, or the workload's `open`, with no answer within `--step-timeout` (30s by default) counts as `failed: no answer within <ms> ms` and the worker moves on, so one stuck step can't hold the run open. The report has one row per outcome, and a failed step counts as `failed: <error tag>`, so failures and losses stay in the table. Each row and the total give steps, steps per second over the whole run, and p50, p90, p99 and max latency in milliseconds. Cost per row is always `unmeasured`.\n\nA local run's output starts with a `LOCAL RUN` notice: the numbers describe this machine and this checkout, not Akter Cloud. A Cloud run's starts with `AKTER CLOUD RUN`: every latency includes the network path from the machine that ran the bench to the app. The report also names the hardware, the runtime, the versions of `@akterjs/cli`, `@akterjs/client`, `@akterjs/effect` and `effect`, the git commit and whether the worktree is clean, and the SHA-256 of the nearest lockfile. `--json` prints the same report as one JSON object, with `\"local\": true` for a local run and `false` for a Cloud run.\n\n## Operator commands\n\nThe old `inspect`, `log`, `snapshot` and `retention` command groups aren't registered. Local inspection is the read-only listener started by `dev`; see [Operating locally](../guides/operating.md).\n\n## Forking an actor\n\n`akter actors fork --url <app url> --actor <Actor> --key <key> --into <new key>` copies an actor into a new actor at another key: its state and files at one moment, with none of its work in flight, since its jobs, timers and undelivered messages stay with the original. It prints the original's commit version at the cut. A retry with the same `--command-id` makes one copy, and `--dry-run` prints the request without sending it. There is no top-level `akter fork`; don't use the old history-copying examples against a 1.0 actor folder.\n\nOn the local engine the copy also gets each of the actor's computers: its disk as it was at the cut, stopped, so no process the original ran runs in the copy. While it copies them the fork holds the original's computers paused under the fork's id: a step already running finishes first, and one sent meanwhile fails with `ComputerHeldError` naming the fork's id. Then each computer goes back to where it was. If the engine stops partway through a fork, the hold stays until a retry with the same id finishes the fork. A fork whose copies would pass the store's limit of 4 computers is refused with `InvalidRequestError`. Akter Cloud copies each computer by its disk alone too, but to do that it stops the original's computer first, so one that was running starts again from its disk with its memory and processes gone, and one that was paused stays stopped. A copy that fails keeps the hold until a retry with the same fork id copies it. Akter Cloud refuses, with `ActorUnavailableError`, a fork whose copies would pass the environment's limit of 4 computers, and a fork with computers into a key on another shard.\n\nWhere the fork runs depends on the target, the same rule every actor command and `akter bench` use. A loopback `--url` (or `AKTER_APP_URL`), such as the address `akter dev` prints, is the local engine: the command sends the actor's fork RPC, `<Actor>/fork`, with no sign-in and no question. Any other target is Akter Cloud, where forking is an operator action (decision 5): the gateway refuses the fork RPC for every caller, so the command runs the Cloud API operation `actorForks.create` instead, `POST /v1/organizations/{organization}/apps/{app}/environments/{environment}/actors/{actor}/{key}/forks` with `into` and `forkId`:\n\n```sh\nakter actors fork --organization acme --app shop --environment prod --actor Sale --key s1 --into s1-try\n```\n\nIt signs in with your credentials (`AKTER_API_KEY` or `akter login`) at the Cloud API (`AKTER_CLOUD_URL` or the URL `akter login` saved); `--organization` defaults as for every Cloud command, and `--command-id` is sent as the fork id. Like every dangerous Cloud command it asks the person at the terminal to confirm, and exits 4 when no terminal can ask. An API key's fork also needs its creator's approval: at a terminal the command asks for the emailed code and retries once with the token, and without one it takes `--approval <token>` from `akter approvals request` and `akter approvals confirm`. The key needs the `fork` scope. On the hosted MCP server the same fork is the `actors_fork` tool, which takes `organization`, `app`, `environment`, `actor`, `key`, `into` and `forkId` and asks the person before anything is sent. The Effect and Promise clients' `fork(into)` stays an actor RPC, so it forks on `akter dev` and is refused on Akter Cloud by design.\n\nCloud forks are a preview behind `cloudFork`, default off. The CLI leaves `actors fork` out of help but still accepts it by name; control refuses an organization without the flag. MCP currently hides `actors_fork` from everyone until it can read organization flags, without falling back to the forbidden actor RPC. Local forks do not use a flag.\n\n## Cloud API commands\n\nEvery Cloud API endpoint is one command, `akter <group> <operation>`, named after the API's own groups and operations: `akter apps list`, `akter apps create`, `akter deploys get`, `akter members change-role` and so on. `akter --help` lists the groups and `akter <group> --help` their operations. The same list, with the same names, descriptions, input schemas and safety hints, is what Akter's MCP tools are built from, so a command and its tool always agree.\n\n- Each field of the operation's input is a flag, such as `--organization` and `--name`. `--input '<json>'` takes the whole input as one JSON object, the same arguments the MCP tool takes; `--input @file.json` reads a file and `--input -` reads stdin. Flags override its fields. Pass a secret, such as a `secrets set` value, with `--input -` or `--input @file.json` rather than a flag, so it stays out of the process list and shell history; `--dry-run` prints the body, secret included. The CLI checks the input against the operation's schema before it sends anything and exits 2 when it doesn't match.\n- `--json` prints the API's answer as one line of JSON in the API's own schema, after checking it against that schema; an answer that doesn't match exits 1, because the CLI and the API disagree. Without `--json` the answer is printed indented. A refusal prints the API's error body with `--json`, and one line on stderr otherwise. After a 503 on a change that is safe to repeat, such as `akter actors erase`, that line adds that it may have stopped partway and to run the same command again.\n- A read that pages takes `--after` and answers with `next` while there is more: `akter actors list`, which lists an app's actors by name and key (`--actor Room` for one name, `--environment` for an environment other than `prod`), and `akter actors history`. An `akter actors list` page holds `--limit` actors (500 at most, and by default) unless it is the last, and the last page has no `next`, so read on until `next` is absent. `--all` does that: it sends each page's `next` as `--after` until a page has none and prints one answer in the API's schema with every page's entries and no `next`. A refused page stops it with that page's error and exit code.\n- Commands that change something take `--dry-run`, which checks the input and prints the request instead of sending it.\n- Destructive commands, such as `akter apps delete`, ask a person to confirm. When stdin or stdout isn't a terminal they never ask: they exit 4 at once, unless `--approval <token>` carries an approval for the action.\n- Signed in with an API key, a dangerous action also needs its creator's approval, and the API refuses it with 428 `ConfirmationRequiredError` until a token comes with it. At a terminal the CLI asks the API to email the key's creator a code, asks you to type it, turns it into a single-use token and sends the command once more; it never prints the token. A refused code exits 3. Without a terminal it exits 4 with reason `ApprovalRequired` and names the steps: `akter approvals request` emails the code, `akter approvals confirm` turns it into a token, and the command runs again with `--approval <token>`.\n- `--organization` defaults to `AKTER_ORGANIZATION`, then the organization saved by `akter login`; `--url` to `AKTER_CLOUD_URL`, then the saved URL, then `https://api.akter.dev`.\n\n## `akter computers`\n\n`akter computers <list|get|logs|pause|resume|stop|delete> --url <app url> --actor <Actor> --key <key>` reaches one actor's computers over its `<Actor>/computer/<op>` RPCs, and each operation is also the `computers_<op>` MCP tool. Every operation but `list` takes `--computer <name>`. `logs` prints a page of the computer's log, oldest first, each line with its `seq`; `--after <seq>` reads on after the last line printed and `--limit` takes 1 to 1000 lines. Each computer's lines also reach the app's log with source `computer` and subject `<environment>/<Actor>/<key>/<computer>`, so `akter logs follow --app <app> --source computer` follows every computer of the app at once. `pause` keeps memory and disk, `stop` keeps the disk, and `resume` fails while a fork holds the computer. There is no command to make a computer or run one of its commands: only the actor's own jobs do that.\n\n`delete` removes the computer and its disk, so it asks a person to confirm, exits 4 when stdin or stdout isn't a terminal, and prints the request without asking under `--dry-run`.\n\n## `akter logs follow`\n\n`akter logs follow --app <app>` prints an app's log lines as they arrive, oldest first, one per line: when, the level, what wrote it and what it's about, then the message. It keeps reading after the last line it printed and waits two seconds when nothing new has come. It runs until interrupted, or until `--count <n>` lines have printed. `--deploy`, `--since` and `--source` narrow it as they do `akter logs read`; the sources are `deploy`, a deploy's progress with its id as the subject, and `computer`, what happened to one of the app's computers. `--json` prints each line as one line of JSON in the API's `LogLine` schema.\n\nIt never prompts. A timeout, a dropped connection, 429 or 5xx is retried with backoff, and each retry is noted on stderr. Any other refusal ends it with the exit code below, such as 1 for an app that doesn't exist and 3 for a key without access.\n\n## Signing in\n\nEvery Cloud command, `deploy` and `export` included, signs in the same way:\n\n1. `AKTER_API_KEY`, an API key or an agent key, when it is set and not empty. Scripts, CI and agents use this. When `AKTER_CLOUD_URL` is set too, the key goes only to that origin: a `--url` naming another server exits 2 with `ApiKeyForOtherUrl`, naming both origins, instead of sending the key there. Drop `--url`, or point `AKTER_CLOUD_URL` at the server you mean.\n2. Otherwise the session or key `akter login` saved. It goes only to the Cloud URL it was saved for: a `--url` or `AKTER_CLOUD_URL` that names another server exits 3 with `LoginForOtherUrl` instead of sending it there. Sign in for that URL, or set `AKTER_API_KEY`.\n\n`akter login --email <address>` emails a six-digit code and asks for it when a person is at the terminal; `akter signup` is the same, for an address with no account yet, since the first sign-in creates it. Without a terminal, as an agent runs it, the command emails the code, prints how to finish (with `--json`, `{ \"codeSent\": true, \"email\", \"expiresAt\" }`) and exits 0 without waiting; `akter login --email <address> --code <code>` then finishes. A refused code exits 3 and saves nothing; `--email` missing without a terminal exits 2. A code that creates an account first needs Akter's terms of service and privacy notice accepted, which `akter docs legal/terms` and `akter docs legal/privacy` print. The CLI asks a person at the terminal once, naming their version, and then sends the same code again; `--accept-terms` accepts them without asking, for an agent. Without a terminal or the flag, it exits 4, names the flag and saves nothing, and the code still works for the next run. A sign-in to an existing account is never asked. The session's secret is saved like a key, below, and never printed: `--json` prints the account, whether this sign-in `created` it, the session's id and expiry, and where it was saved.\n\n`akter login --with-api-key --url <cloud-url>` saves a key instead: it reads the key from stdin or a hidden prompt, never from a flag, checks it with the Cloud API and saves it. The key goes to the OS keychain (macOS Keychain, or the Secret Service through `secret-tool` on Linux) when one is reachable, and otherwise into `$XDG_CONFIG_HOME/akter/login.json` (`~/.config/akter/login.json`), which only you can read (mode 0600). `akter logout` removes it.\n\nWithout either, a Cloud command exits 3.\n\n## `akter docs`\n\n`akter docs` lists the topics of these docs, and `akter docs <topic>` prints one page as Markdown, such as `akter docs reference/cli`, or `akter docs cli` when only one page has that name. `--json` prints the list, or the page with its topic and title, as one line of JSON. An unknown topic exits 2 and lists the topics that contain it.\n\n## Exit codes\n\n| Code | Meaning |\n| --- | --- |\n| 0 | Done. |\n| 1 | Failed: the API refused (404, 409 and the like), the answer broke its schema, or a command such as `deploy` ended without success. |\n| 2 | Usage: an unknown flag, a missing or invalid input, or a 400 or 422 from the API. |\n| 3 | Authentication: not signed in, the API answered 401 or 403, including when it refused the key at sign-in, or an actor refused the caller, by its kind (`CallerNotAllowedError`) or by its `files.authorize` rule (`FileAccessDeniedError`). |\n| 4 | Confirmation: a person had to confirm or approve and couldn't be asked, or declined; also a 428 from the API. |\n| 5 | Unavailable: the API couldn't be reached, timed out, or answered 408, 429 or 5xx, including at sign-in. Retrying may help. Every Cloud command's sign-in first retries a 5xx for about 30 seconds, so it rides out a Cloud deploy. |\n| 130 | Interrupted. |\n\n`deploy` and `export` use the same codes when the API answers with a status, and 1 when it can't be reached; signing in, their first step, exits 3 or 5 as above. Treat a nonzero exit as failure and retain its diagnostic. With `--json`, a failure the CLI detects itself prints one JSON object with `code`, `reason` and `message`. Help exits successfully, deployment success requires the terminal `Live` notice, and an export is complete only when it prints `Exported <n> actors to <folder>` and exits 0. An uploaded build, a printed deployment id or an interrupted follow isn't equivalent to a successful deployment.\n",
|
|
127
127
|
"topic": "reference/cli"
|
|
128
128
|
},
|
|
129
129
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akterjs/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "The akter command: local engine, deploy, branch, restore, history, export, migrate and bench.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"esbuild": "0.28.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@akterjs/client": "0.1.
|
|
35
|
+
"@akterjs/client": "0.1.1",
|
|
36
36
|
"@akterjs/cloud-api": "file:../../../akter-cloud/packages/cloud-api",
|
|
37
|
-
"@akterjs/effect": "0.1.
|
|
37
|
+
"@akterjs/effect": "0.1.1",
|
|
38
38
|
"@effect/openapi-generator": "4.1.2",
|
|
39
39
|
"@effect/platform-node": "4.1.2",
|
|
40
40
|
"@effect/vitest": "4.1.2",
|