6 sheets · 42 rows · 26 new ideas · 39 with a code line

What's inside

Every row is one thing arki or Dot does today, with the package it lives in and the line of code that shows it.

  • shippedon npm now
  • candidatein the next release
  • plannedon the roadmap
SheetI-101

Dot: the app kernel

Dot composes an app from typed units, and the compiler checks the wiring before anything runs.

10 rows · 9 new ideas

  • shipped
    new idea

    Provider order checked by the compiler

    Mount a unit before the one it needs, or the editor rejects that line and the error says where to move it.

    @arki/dot

    packages/dot/src/define-app.tslines 42-56, 93-98
    import type {  AnyUnit,  EmptyPhases,  EmptyShape,  MergePhases,  RuntimeUnit,  ServiceRecord,  UnitCloseFailure,  UnitOpenFailure,  UnitPhaseFailures,  UnitPhases,  UnitRole,  UnitSourceGuard,  UnitWiring,} from './unit-contract.js';type NeedsError<TAvail extends ServiceRecord, TNeeds extends ServiceRecord> = {  readonly 'DOT: this unit needs services that nothing before it provides. Mount their providers earlier (.infra → .features → .entrypoints)': {    readonly missing: MissingKeys<TAvail, TNeeds>;    readonly mismatched: MismatchedKeys<TAvail, TNeeds>;  };};
  • shipped
    new idea

    Key collisions caught at compile time

    Two units that provide the same key do not type-check; rename() gives one of them a new key and rebuilds its handles.

    @arki/dot

    packages/dot/src/define-app.tslines 100-103
    type CollisionError<TAvail, TProvides> = {  readonly 'DOT: this unit provides a key that an earlier unit already provides. Give one of them a new key with rename()': keyof TAvail &    keyof TProvides;};
  • shipped
    new idea

    Typed service handles

    A unit names what it needs as handles like database.provides.db; the type comes from the provider, and .lazy defers the value until first use.

    @arki/dot

    packages/dot/docs/di.mdlines 646-649
      .needs({    db: database.provides.db, // typed from the provider    cache: cache.provides.kv.lazy, // a Lazy<T>, even if the provider is eager  })
  • shipped
    new idea

    entrypointOnly: infra the usecases never see

    A feature can take db or kv for its own store and limiter while its usecases only pick the services they name.

    @arki/dot

    apps/avocabo/backend/src/features/waitlist/feature.tslines 12, 17-19, 34-39
    import { defineFeature, entrypointOnly } from '@arki/dot/feature';import { database } from '../../infra/db';import { cache } from '../../infra/kv';import { emailClient } from '../../infra/mail/client';  .needs({    db: entrypointOnly(database.provides.db),    kv: entrypointOnly(cache.provides.kv),    // The app's outbound email client; a test fakes the plugin.    mailer: emailClient.provides.emailClient,  })
  • shipped
    new idea

    Stable error codes

    Every kernel failure has an ARKI_DOT_E### code that is never renumbered or reused, so a link to its page stays valid.

    @arki/dot

    packages/dot/src/lifecycle.tslines 50-55
     * Stable error codes for every failure the kernel throws, `ARKI_DOT_E001`.. * Codes are never renumbered and never reused (E009 and E010 are retired). * E001-E021 and E033 are lifecycle failures; E022-E031 are feature-composition * failures (see `FEATURE_ERROR_CODES`); E032 is non-JSON manifest data. */export const DotLifecycleErrorCode = {
  • shipped
    new idea

    Wrong order caught at configure, before boot

    A provider mounted after its consumer fails as ARKI_DOT_E020 while the app is still being configured; no connection has opened yet.

    @arki/dot

    packages/dot/docs/diagnostics.mdlines 218-220
    at `configure()`:- `ARKI_DOT_E020` — a provider is mounted after its consumer. The fix
  • shipped
    new idea

    boot() returns a failure, never throws

    A failed boot comes back as a typed value; the units already booted are disposed in reverse order and the app is marked failed.

    @arki/dot

    packages/dot/docs/lifecycle.mdlines 100-102
    - Previously booted units dispose in reverse declaration order.- App transitions to `failed`; no failed provider publishes a service.- The original refusal is returned when cleanup succeeds; unknown throws become
  • shipped
    new idea

    dot explain and dot doctor

    explain prints the app manifest from configure alone; doctor boots the app, runs its checks and disposes it; both answer in JSON.

    @arki/dot

    packages/dot/docs/cli.mdlines 9-10
    | `dot explain` | Imports the app file and runs `configure`. Never boots.                                    || `dot doctor`  | Boots the app (`boot` hooks run, migrations apply), then disposes it. Never calls `start`. |
  • shipped
    new idea

    TanStack Start as the driving adapter

    Every Start request gets a typed context: the session, an RPC client and the mounts, built from the app's own services.

    @arki/dot

    apps/avocabo/backend/src/app.tslines 18, 23, 26, 42-47
    import { tanstackStart } from '@arki/tanstack-start/dot';import { gateway } from './entrypoints/gateway';import { authn } from './infra/auth';  .entrypoints(    hostClock,    tanstackStart({      needs: { rpc: gateway.provides.rpc, auth: authn.provides.auth, scheduler },      session: ({ auth }, request) => auth.principal(request.headers),      rpc: ({ rpc }, request) => rpc.client(request.headers),
SheetI-102

Journal: the write path

Every change is one immutable event next to a normal row, and both are written in one transaction.

12 rows · 10 new ideas

  • shipped
    new idea

    One transaction for row, events, projections and jobs

    A write appends the events, saves the row, runs the inline projections and queues the after-commit jobs, and all of it commits or rolls back together.

    @arki/journal

    packages/journal/src/write.tslines 377-381
            for (const event of recorded) {          for (const projection of projections) {            if (projection.types.has(event.type) && projection.entities.some(e => e.kind === entity.kind)) {              await projection.run(sp, event);            }
  • shipped
    new idea

    Commands refuse with typed failures

    A rule that says no returns a failure; the write rolls back and run returns it with its type, so every caller must handle it.

    @arki/journal

    packages/journal/README.mdlines 46, 85-90
    import { CommandConflict, CommandNotFound, initJournal, need } from '@arki/journal';    write(order, input.orderId, row => {      if (!row) return CommandNotFound({ detail: 'Order not found' });      if (row.status !== 'placed') return CommandConflict({ detail: `Order is already ${row.status}` });      return [OrderPaid({ amount: input.amount })]; // [] means: write nothing    }),);
  • shipped
    new idea

    Reducers patch the row; null deletes it

    An entity maps each event to the columns that change; returning null removes the row while its events stay in the log.

    @arki/journal

    packages/journal/README.mdlines 75-77
      .create(OrderPlaced, e => ({ customerId: e.data.customerId, total: e.data.total, status: 'placed' }))  .on(OrderPaid, () => ({ status: 'paid' })) // a patch: only the columns that change  .on(OrderCancelled, () => null); // null deletes the row; the events stay
  • shipped
    new idea

    Idempotent commands and HTTP replies, one receipts table

    journal_receipts answers a retried command with its first result, and the same table caches a whole HTTP response for the idempotent() middleware.

    @arki/journal

    packages/journal/README.mdlines 191-194
    import { idempotent } from '@arki/http/idempotency';import { responseCache } from '@arki/journal/http';idempotent({ cache: responseCache(db, tables), scope, routes: ['POST /orders'], ttl: 86_400_000 });
  • shipped
    new idea

    Wake channel on commit

    Each commit that recorded events calls pg_notify with the seq range, so every listening worker in every process drains at once.

    @arki/journal

    packages/journal/src/write.tslines 7, 410
    import { and, eq, sql } from 'drizzle-orm';          sql`select pg_notify(${deps.wakeChannel}, ${JSON.stringify({ from: Math.min(...seqs), to: Math.max(...seqs) })})`,
  • candidate
    new idea

    Durable workflows are journal entities

    A workflow run is an entity; every step, wait and undo is an event in journal_events, and each step runs as a job that survives a restart.

    @arki/journal

    packages/journal/README.mdline 810
    A workflow is a list of steps that runs to the end even when the process dies. Each step runs as a job in `journal_jobs`. When a step finishes, its result is saved (a checkpoint) and the next step is scheduled, in one transaction. A run can wait for a signal from outside, or sleep, for hours or days. When it cannot finish, it undoes the steps it did, newest first (compensation). The run is a journal entity (`workflow.run`), so every move is an event in `journal_events`.
  • candidate
    new idea

    A workflow starts from a domain event

    startOn ties a run to an entity event; the run id and its input come from that event, in the transaction that recorded it.

    @arki/journal

    packages/payments/src/refunds/workflow.tslines 124-129
            startOn: {          entity: refund,          event: refundsJournal.events.RefundApproved,          id: e => refundRunId(e.entityId),          input: e => ({ refundId: e.entityId }),        },
  • candidate
    new idea

    waitFor a signal, with a timeout

    A run can wait days for an outside signal, check it against a schema, and carry on or stop when the time runs out.

    @arki/journal

    packages/payments/src/refunds/workflow.tslines 201-206
          .waitFor('settled', {        signal: 'stripe.refund.settled',        schema: settledSchema,        timeout: '3d',        // No signal in 3 days: `record` asks Stripe        onTimeout: 'continue',
  • shipped
    new idea

    Erasure that keeps history consistent

    forget rewrites an entity's events to their erased form and repairs the row from the rewritten log, in one transaction.

    @arki/journal

    packages/journal/README.mdline 262
    `journal.forget(db, entity, id)` rewrites every stored event of that entity that has an erased form to it, sets `erased_at` on those rows, and then repairs the row from the rewritten log, so the row and its history agree. All in one transaction. An erased form the schema rejects is `BAD_DEFINITION` and nothing changes. A history replay cannot rebuild, such as a legacy row that was never adopted, is `NO_HISTORY` and nothing changes: adopt it first. Running it twice is safe; the first erased time is kept. It returns `{ erased, row }`.
  • shipped
    new idea

    The journal as a Dot plugin

    Add the journal to a Dot app with two plugins: journal() gives features a services.journal to write through, and journalWorker() runs the effects, taps and async projections, handing each the services of the feature that lists it.No app uses it yet.

    @arki/journal

    packages/journal/src/dot.tslines 612-617
     * The entrypoint, for `.entrypoints()`: one worker between `start` and `stop`, for the * journal that `journal(...)` published. Each effect and tap of a feature gets the * services of that feature. A process that serves no worker still writes: its jobs * wait for a process that does. With `jobs`, the worker also runs queue rows * (`@arki/queue/outbox`) through the runner the source builds at boot. */
SheetI-103

Data

Postgres through Drizzle, a key-value store with a fallback, and config that carries its types.

4 rows · 1 new ideas

  • shipped
    new idea

    Encrypted env files you can commit

    Secrets live encrypted in .env.<environment> files; a public key in the file lets anyone add a value, and only the private key reads them.

    @arki/env

    packages/env/README.mdline 85
    Commit `.env.<environment>` files with their secrets encrypted. The format is dotenvx 2.x, so the `dotenvx` CLI reads the same files. Each file holds a public key, so anyone can add a value. Only a holder of the private key can read one. The private key lives in `DOTENV_PRIVATE_KEY_<ENV>` (for `.env.production`: `DOTENV_PRIVATE_KEY_PRODUCTION`) or in the gitignored `.env.keys` file.
SheetI-104

Web and UI

Typed HTTP you can test without a socket, and components built on one token vocabulary.

3 rows · 1 new ideas

  • shipped
    new idea

    An engine you can call without a socket

    buildEngine returns a fetch function; a test passes a Request and reads the Response, and no server is started.

    @arki/http

    packages/http/README.mdlines 28, 39-41
    import { buildEngine, route, routes } from '@arki/http';const engine = buildEngine([routes().bind(getOrder, ({ params }) => ({ id: params.id, total: 12 }))]);const response = await engine.fetch(new Request('http://localhost/orders/ord_1'));
SheetI-105

Money and payments

Amounts are whole units, currencies are kept apart by the type, and payments write through the journal.

3 rows · 1 new ideas

  • shipped
    new idea

    Exact units, a number until bigint is needed

    19.99 EUR is 1999 units; nothing rounds unless you ask, and a plain number is used up to 2^53.

    @arki/money

    packages/money/README.mdline 5
    - **Exact.** Amounts are whole numbers of units: `19.99 EUR` is `1999` units at 2 decimals. Nothing rounds unless you pass a rounding mode.
SheetI-106

Platform

The small packages every layer stands on: failures, schemas, logs, the command line and time.

10 rows · 4 new ideas

  • candidate
    new idea

    Failures without a wrapper

    A function returns the value itself or a failure; there is no ok() to add and no .value to unwrap.

    @arki/failure

    packages/failure/README.mdlines 24, 26, 28-32
    import { z } from 'zod';import { defineFailure, isFailure } from '@arki/failure';const EmailTaken = defineFailure('user.email_taken', {  status: 409,  data: z.object({ email: z.string() }),  message: 'email {email} is taken',});
  • candidate
    new idea

    The failure union is inferred

    A function's return type lists every failure it can produce, including the ones it passes on; nobody writes the union down.

    @arki/failure

    packages/failure/README.mdline 48
    `signUp` returns `{ email } | Failure<'user.email_taken', { email: string }>`. Nobody wrote that type. The compiler infers it, and `result.email` is a type error until `isFailure` has run.
  • candidate
    new idea

    A lint rule catches an ignored failure

    must-use reports a call whose failure is thrown away; no-blind-accept reports an accept of a failure not checked by kind.

    @arki/failure

    packages/failure/README.mdline 9
    - **Caught when ignored.** The `must-use` lint rule reports a call whose failure is thrown away. `no-blind-accept` reports `accept(r)` of a failure not checked by kind.
  • candidate
    new idea

    A JSON envelope when piped, text on a terminal

    One defineCommand gives the flags, their coercion and the help from the input schema; a run writes JSON when stdout is a pipe.

    @arki/cli

    packages/cli/README.mdline 3
    Typed command-line programs from one `defineCommand({...})`. The input schema drives the flags, their coercion and their help. A run writes a JSON envelope when stdout is a pipe and text on a terminal. Exit codes are stable. Help and `--version` never run a command, and import at most one lazy level of the tree.