# Quickstart

URL: https://docs.domainruntime.dev/docs/introduction/quickstart
Status: Planned
Reviewed: 2026-09-22



<div className="docs-note">
  **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.
</div>

## Create an app [#create-an-app]

```bash
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.

```text
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 [#add-an-entity-and-an-action]

Open `src/domain.ts`:

```ts title="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.

<details>
  <summary>
    Today
  </summary>

  `runtime.tx` is planned. Today the body is:

  ```ts
  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.
</details>

## Read and write from the page [#read-and-write-from-the-page]

```tsx title="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>
  );
}
```

```ts title="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](/docs/auth/permissions) yet, so anyone with the URL can read it. Add rules before you share the URL.

## Change the domain [#change-the-domain]

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

```ts title="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 };
      },
    }),
```

```tsx
<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 [#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 [#next]

* [How it works](/docs/introduction/how-it-works) — the model and its guarantees, in one table.
* [Modeling data](/docs/data/modeling-data), [Reading data](/docs/data/reading-data), [Actions](/docs/data/actions) — the three things you just did.
* [Guarantees](/docs/data/guarantees) — what is atomic, what runs once, what is live.
