DomainRuntimedocs
Introduction
PlannedWhat works todayReviewed 2026-09-22

Quickstart

From an empty folder to a live environment, then change an action and watch it update.

Planned for public use. This flow runs against production since 2026-09-22; it becomes public when the CLI is published to npm. Until then, write to us for access.

Create an app

npx @domainruntime/cli create-app my-app --next
cd my-app
npx druntime        # or: npm run local

druntime opens your browser to sign in — or create an account, which gives you a personal organization — and creates a project and a personal development environment the first time. Then it:

  1. publishes src/domain.ts to your environment,
  2. writes the environment's URL into .env.local as DOMAIN_RUNTIME_URL,
  3. starts your Next.js dev server,
  4. watches your domain and republishes on every save.
my-app  ·  dev-my-app-dev-1a2b3c4d-7f3k9x2m
backend   https://dev-my-app-dev-1a2b3c4d-7f3k9x2m.domainruntime.cloud   published 0.6 s ago
web       http://localhost:3000                                          ready

p publish   r restart clients   q quit (keeps the environment)

Add an entity and an action

Open src/domain.ts:

src/domain.ts
import { domain, defineAction, i } from "@domainruntime/domain";
import { z } from "zod";

export default domain("app")
  .withSchema({
    entities: {
      app_todo: i.entity({
        text: i.string(),
        done: i.boolean(),
        createdAt: i.date(),
      }),
    },
    links: {},
    rooms: {},
  })
  .withActions({
    addTodo: defineAction({
      input: z.object({ todoId: z.string().uuid(), text: z.string().trim().min(1).max(200) }),
      output: z.object({ todoId: z.string() }),
      async execute({ input, runtime }) {
        await runtime.tx((tx) =>
          tx.app_todo[input.todoId].update({ text: input.text, done: false, createdAt: Date.now() }),
        );
        return { todoId: input.todoId };
      },
    }),
  });

Save the file. druntime publishes it; the action exists in your environment.

Today

runtime.tx is planned. Today the body is:

const db = await runtime.db();
await db.transact(db.tx.app_todo[input.todoId].update({ text: input.text, done: false, createdAt: Date.now() }));
return { todoId: input.todoId };

The current scaffold also contains src/runtime.ts, instant.schema.ts and an /api/domain route from the standalone adapter; druntime does not need them.

Read and write from the page

src/app/page.tsx
"use client";
import { db } from "@/lib/db";

export default function Page() {
  const { isLoading, error, data } = db.useQuery({ app_todo: {} });
  if (isLoading) return null;
  if (error) return <p>{error.message}</p>;

  return (
    <main>
      <form
        onSubmit={async (e) => {
          e.preventDefault();
          const form = e.currentTarget;
          const text = new FormData(form).get("text") as string;
          await db.actions.app.addTodo({ todoId: crypto.randomUUID(), text });
          form.reset();
        }}
      >
        <input name="text" placeholder="What needs to be done?" />
      </form>
      <ul>
        {data.app_todo.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </main>
  );
}
src/lib/db.ts
import { init } from "@domainruntime/react";
import app from "@/domain";

export const db = init({ domain: app });

Open http://localhost:3000, add a todo, then open a second tab: both update live.

Your new entity has no permission rules yet, so anyone with the URL can read it. Add rules before you share the URL.

Change the domain

Add a second action that takes the todo's id and flips done:

src/domain.ts
    toggleTodo: defineAction({
      input: z.object({ todoId: z.string().uuid() }),
      output: z.object({ done: z.boolean() }),
      async execute({ input, runtime }) {
        const { app_todo } = await runtime.query({ app_todo: { $: { where: { id: input.todoId } } } });
        if (!app_todo[0]) throw new Error("That todo no longer exists");
        const done = !app_todo[0].done;
        await runtime.tx((tx) => tx.app_todo[input.todoId].update({ done }));
        return { done };
      },
    }),
<li key={todo.id} onClick={() => db.actions.app.toggleTodo({ todoId: todo.id })}>
  {todo.done ? <s>{todo.text}</s> : todo.text}
</li>

Save. The next click runs the new action against the same environment and the same data. Nothing is redeployed or reprovisioned.

Submit an empty todo: the runtime rejects it before your code runs, with action_contract_validation_failed.

What just happened

  • The page reads with a live query. Both tabs update because the environment pushes every change to every subscriber.
  • The page writes only by calling actions. There is no transaction in the browser; every change is a named operation with validated input.
  • The domain file is the backend. Saving it publishes your actions.

Next

On this page