Skip to content

The framework for durable, stateful backends that power realtime apps, background work, and agents.

bun add @rikalabs/akter@alpha
STATEtotal ยท chargeIdTABLEorder_linesJOBSChargeEVENTSOrderPlacedORDR 008F2C 4 ยท Order/ord_8f2c12345

An actor framework for TypeScript.

An actor is a container for one thing in your app: an order, a chat room, an agent session. Everything that thing needs is packed inside.

  1. 1An address. Order/ord_8f2c, reachable from anywhere.
  2. 2Commands, one at a time. Each one commits in its own transaction.
  3. 3State and tables. Rows in your Postgres, owned by this actor.
  4. 4Events and jobs. Durable, retried, and run after the commit.
  5. 5Live clients. Connections that stay open while it sleeps.

Define it, handle it, call it.

Define the actorsrc/order/contract.ts
export const Order = Actor.make("Order", {
  key: Schema.NonEmptyString,
  state: Actor.state({
    total: Schema.optional(Schema.Int),
    chargeId: Schema.optional(Schema.String),
  }),
  tables: [orderLines],
  events: [OrderPlaced],
  jobs: { Charge: { job: Charge, onSuccess: Charged } },
  api: { Place },
  internal: { Charged },
})
Call it from anywheresrc/checkout.ts
export const checkout = Effect.gen(function* () {
  const order = yield* Order.get("o-1")

  return yield* order.Place({ lines: [{ sku: "mug", quantity: 2, unitPrice: 1200 }] })
})
Handle a commandsrc/order/layer.ts
export const OrderLive = Order.toLayer({
  Place: Effect.fn(function* ({ lines }) {
    const turn = yield* Order.Turn

    if (turn.state.total !== undefined) return yield* AlreadyPlaced.make({})

    const total = lines.reduce((sum, line) => sum + line.quantity * line.unitPrice, 0)

    yield* turn.rows(orderLines).insert(lines)
    yield* turn.emit(OrderPlaced.make({ total }))
    yield* turn.enqueue(Charge.make({ amount: total }))
    yield* turn.state.set({ total })

    return total
  }),

  Charged: Effect.fn(function* ({ chargeId }) {
    const turn = yield* Order.Turn

    yield* turn.state.set({ chargeId })
  }),
})

One command, one transaction.

Committed one transaction
one turn, one transactionCommandPlace({ lines })with a command IDFencestill the owner?checked in the DBReceiptseen this ID?replay the resultHandleryour Effect runsrows ยท events ยท jobsCommitstate ยท rows ยท eventsreceipt ยท outboxReplythe result, 2400after the commitDeliver messagesFire timersRun jobsafter the commitone turn, one transactionCommandPlace({ lines })with a command IDFencestill the owner?checked in the DBReceiptseen this ID?replay the resultHandleryour Effect runsrows ยท events ยท jobsCommitstate ยท rows ยท eventsreceipt ยท outboxReplythe result, 2400after the commitDeliver messagesFire timersRun jobsafter the commit
Actor
Order/o-1
Command
Place ยท 1 line
Result
2400
Committed
rows ยท OrderPlaced ยท state
After commit
Charge โ†’ Charged

A chat room is an actor.

Rooms, documents and dashboards that keep their history and push every change to connected clients.

A billing run is an actor.

Payments, imports and billing runs that retry with backoff, run on schedules, and never quietly vanish.

An agent session is an actor.

Sessions that keep their transcript, pause for an approval, and resume after a crash or a deploy.

One hot key, 64 callers

Successful writes per second, median of three runs on the same 4-CPU sandbox.

Akter1,647 op/s
Rivet default1,141 op/s
Rivet saved1,113 op/s
workerd (local)975 op/s
DBOS770 op/s
Restate368 op/s
Temporal2.6 op/s *
Plain-store baselinesPostgres 5,213 ยท Redis 24,293 op/s

* Temporal: 171 measured errors; the figure counts successful requests only.

1.62 mssequential write p50
0.40 msfresh read p50
791 op/s10,000 fresh keys

Same-host observations, not production SLOs. Plain Postgres and Redis were faster, and Restate was faster across 10,000 keys. Full method in BENCHMARKS.md.

Read the benchmark report โ†’
192,794acknowledged IDs
0lost
0duplicated
0unknown

Process kills and network partitions, every acknowledgement checked afterwards.

Actor.makeIdentity, state and tables
Actor.commandTyped, one at a time
Actor.jobRetries and schedules
Actor.eventDurable, with a cursor
Actors.serveEmbedded, served or hosted
Start building (quickstart)

Your first actor, running on your Postgres, in a few minutes.

Read the docs โ†’
01What is an actor, exactly?

An addressable part of your app, such as one order, one room or one agent session. It handles one command at a time and owns its data, its background work and its live connections.

02Do I need Postgres?

Your data lives in Postgres, as ordinary tables you can query with plain SQL. To start, PGlite, an embedded Postgres, runs the same code with no Docker or database server; Database.postgres points it at a real server. In the alpha, run one runtime process per database.

03Can I run it inside my existing server?

Yes. Embedded, you provide Actors.layer and call actors as Effects in your own process. Served, Actors.serve exposes the same actors over HTTP, WebSocket and SSE, with an OpenAPI document and an MCP endpoint.

04What happens when a process dies mid-command?

Before the commit, nothing was written, and a retry with the same command ID places the order once. After the commit but before the reply, the retry finds the stored result and returns it without running the handler again.

05How is this different from a workflow engine?

Temporal and Restate record a function's steps so it can resume after a crash. An Akter command is a short transaction instead, and anything slow becomes a job or a workflow owned by the actor.

06Is there a hosted version?

Akter cloud, with managed runners, Postgres and an inspector for every actor, is in development and its pricing here is a placeholder. The framework is Apache-2.0 and runs wherever Bun does.

More questions in the docs โ†’