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

    OTel-first kernel

    Traces, metrics and logs come out of the kernel; you wire only the exporter, and without an SDK the no-op API is used.

    @arki/dot

    packages/dot/docs/observability.mdlines 3-4
    DOT is OTel-first. Traces, metrics, and logs come out of the kernelwithout you wiring anything — you wire the _exporter_. Without an OTel SDK, tracing and metrics safely use the API’s no-op implementations.
  • 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

    Effects after commit, at least once

    An effect runs from a job row once the transaction committed; its jobId stays the same across retries, so you pass it on as an idempotency key.

    @arki/journal

    packages/journal/README.mdline 409
    An effect runs after the commit, at least once, from a row in `journal_jobs`. A handler gets the event and a context: `(e, { jobId, signal, db, run })`.
  • 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) })})`,
  • 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

    Three ways to run Postgres

    node-postgres by default, Bun's native SQL through the bun export condition, or embedded PGlite for local and test runs.

    @arki/db

    packages/db/README.mdline 5
    - Two runtime entry points (`node-postgres` pool by default, Bun-native SQL via the `bun` export condition, embedded PGlite via `./runtime-local`).
  • shipped

    Key-value with a safe fallback

    Keyv underneath, Redis rate limiting, TLS from rediss:// URLs, and an in-memory fallback when Redis is unreachable.

    @arki/kv

    packages/kv/README.mdline 3
    Key-value store abstraction with pluggable adapters for ARKI. Wraps [Keyv](https://keyv.org/) for general key-value access and [`rate-limiter-flexible`](https://github.com/animir/node-rate-limiter-flexible) for Redis-backed rate limiting. Provides automatic TLS handling (via `rediss://` URLs), Upstash support, and a graceful in-memory fallback when Redis is unreachable.
  • 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.
  • shipped

    Typed env with any Standard Schema

    defineEnv validates on the first read and returns a typed object; Zod, Valibot, ArkType or a hand-written schema all work.

    @arki/env

    packages/env/README.mdline 3
    Type-safe environment variables for ARKI. `defineEnv()` wraps [`@t3-oss/env-core`](https://env.t3.gg/). It validates on the first read and returns a typed object. Any [Standard Schema](https://standardschema.dev) works: Zod, Valibot, ArkType or a hand-written `~standard` object.
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

    Route contracts as data, OpenAPI from them

    A route is a value; handlers are validated by any Standard Schema, and the OpenAPI document is generated the same way every time.

    @arki/http

    packages/http/README.mdlines 3-6
    Typed HTTP for ARKI — route contracts as data, handlers validated by anyStandard Schema (zod, valibot, arktype, your own),request-scope derivers, SSE streaming, and deterministic OpenAPIgeneration. Ships a DOT adapter (`@arki/http/dot`) whose `http()` entrypoint unit
  • 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'));
  • shipped

    One import per component

    Base UI primitives styled with Tailwind v4 semantic tokens: forms, overlays, navigation, data display and charts.

    @arki/ui

    packages/ui/README.mdline 3
    React component library for ARKI apps: Base UI primitives styled with Tailwind v4 semantic tokens. Forms, overlays, navigation, data display and charts. One import per component.
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.
  • shipped

    Currencies kept apart by the type

    Money<'EUR'> cannot be added to Money<'USD'>; the compiler stops it, and the runtime throws too.

    @arki/money

    packages/money/README.mdline 7
    - **Safe types.** `Money<'EUR'>` cannot be added to `Money<'USD'>`. The compiler stops it, and the runtime throws `CurrencyMismatchError` too.
  • shipped

    Stripe provider with journal commands

    Plans, customers, subscriptions, payments, refunds, dunning and tax, with table factories, repositories and commands; routers stay in your app.

    @arki/payments

    packages/payments/README.mdline 3
    Payments for ARKI apps: a Stripe provider, plans, customers, subscriptions, payments, refunds, dunning and tax. It ships Drizzle table factories, repositories, services and [`@arki/journal`](https://www.npmjs.com/package/@arki/journal) commands. Routers stay in each consuming app, so the app owns its RPC context and its authorization policy.
SheetI-106

Platform

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

10 rows · 4 new ideas

  • shipped

    Message templates keep values as fields

    A template like 'user {userId} signed in' logs its fields, a processor can redact one, and records map onto the OpenTelemetry log model.

    @arki/log

    packages/log/README.mdline 3
    Zero-dependency structured logging for every JavaScript runtime. Libraries log; apps configure. Message templates keep their values as fields, channels are set in one config, and records map onto the OpenTelemetry log data model. Importing it does nothing: no env read, no output, no listener.
  • shipped

    Temporal dates with no hidden now

    Calendar dates, instants, zoned date-times, durations and business days; values are immutable, and there is no hidden zone.

    @arki/date

    packages/date/README.mdline 3
    Date and time math on [Temporal](https://tc39.es/proposal-temporal/) values with a small, explicit API. Calendar dates, instants, zoned date-times, durations, interval algebra, business days, formatting and relative text. Values are immutable, one arithmetic policy applies everywhere, and there is no hidden "now" and no hidden zone.