---
title: Local development
description: Run authentication on your own machine, or point it at your real production users.
---

The Zeitlos CLI runs a local auth service, so sign-up, sign-in, verification and password
reset all work on your machine without touching production.

```bash title="Terminal"
npx @zeitlosapp/cli dev
```

Requires **Node 22.5+**. It writes `ZEITLOS_AUTH_URL` into `.env.local` for server-side use,
and the auth service listens on port **7311** by default.

## Authentication emails

While using the emulator authentication emails won't be sent. Instead you'll see the link that
would be sent printed to the console where you started `npx @zeitlosapp/cli dev`.

## Password resets

To test the password reset page, you'll need to configure the path locally in the `zeitlos.dev.json` file at the root of your repository. Set the `auth.resetPasswordUrl` to the URL that should
be used:

```json title="zeitlos.dev.json"
{
  "auth": {
    "resetPasswordUrl": "http://localhost:3000/reset-password"
  }
}
```

## Testing against real users

Sometimes the emulator's isolated users aren't what you want and you'd rather sign in as a
real deployed user:

```json title="zeitlos.dev.json"
{
  "project": "my-app",
  "auth": { "useProductionAuth": true }
}
```

The local auth server then proxies to `<slug>.ztls.app` instead of emulating. Nothing in your
code changes — it still talks to the local emulator — but sign-in creates a **real production
session** and `getUser()` returns the real user.

> [!WARNING]
> Because this is production auth: real emails are sent, and their links point at your
> production origin rather than localhost, so the terminal no longer prints them. The other
> `auth.*` settings in `zeitlos.dev.json` are ignored. Your project's real settings apply.

## Settings

Local development is configured in `zeitlos.dev.json` at your project root. These are the same
behaviours you'd set under **Authentication → Settings** in the dashboard, applied locally.

```json title="zeitlos.dev.json"
{
  "auth": {
    "enabled": true,
    "signUpEnabled": true,
    "requireEmailVerification": false,
    "resetPasswordUrl": "http://localhost:3000/reset-password",
    "verifyCallbackUrl": "http://localhost:3000/welcome"
  },
  "ports": { "auth": 7311 }
}
```

| Key                             | Purpose                                                              |
| ------------------------------- | -------------------------------------------------------------------- |
| `auth.enabled`                  | Can be used to disable the auth emulator. `true` by default                                             |
| `auth.signUpEnabled`            | Allow sign-up                                                 |
| `auth.requireEmailVerification` | Require verification before sign-in                                  |
| `auth.resetPasswordUrl`         | Where the printed reset link points — reset stays disabled until set |
| `auth.verifyCallbackUrl`        | Where the verify link lands after verifying                          |
| `auth.useProductionAuth`        | Proxy to your real project's auth instead of emulating               |
| `ports.auth`                    | Run auth on a port other than 7311. **Note:** Changing the port requires more changes to your app outline [below](#auth-calls-go-to-the-wrong-port).                            |

Set `resetPasswordUrl` to your local reset page so the link printed to the terminal actually
lands somewhere. See [Password reset](/authentication/password-reset/).

> [!NOTE]
> The file configures local development only. Nothing in it affects your deployed project.
> The database has its own keys in the same file; see
> [Database → Local development](/database/local-development/).


## Troubleshooting

### Auth calls go to the wrong port

This is the one case the SDK cannot work out on its own. In the browser it can't read
`ZEITLOS_AUTH_URL` — that's a server-only variable — so it assumes the emulator's default port
**7311**. If you changed `ports.auth`, the browser keeps looking at 7311 and every call fails.

Tell the client where to look:

```ts title="lib/zeitlos.ts"
import { createClient } from '@zeitlosapp/sdk';

export const client = createClient({
  // Unset in production, where same-origin resolution takes over — so the same
  // code works in both places.
  authUrl: process.env.NEXT_PUBLIC_ZEITLOS_AUTH_URL,
});
```

Expose the port to the browser using whatever prefix your framework requires (`NEXT_PUBLIC_`,
`VITE_`, …):

```bash title=".env.local"
NEXT_PUBLIC_ZEITLOS_AUTH_URL=http://localhost:7350
```

The same applies to any dev host that isn't `localhost`.

### `forgetPassword` throws `RESET_PASSWORD_NOT_CONFIGURED`

Reset is disabled until a reset page is configured. Locally that means setting
`auth.resetPasswordUrl` in `zeitlos.dev.json` — the dashboard setting only covers production.

### Sign-in throws `EMAIL_NOT_VERIFIED`

`requireEmailVerification` is on. Either verify using the link printed to your terminal, or set
`auth.requireEmailVerification` to `false` while you build.
