> ## Documentation Index
> Fetch the complete documentation index at: https://docs.edgespark.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Develop EdgeSpark apps locally with edgespark dev

> Run the full EdgeSpark stack on your machine with edgespark dev — local D1, R2, auth, hot reload, seed data, and a dev-mode auto-verify endpoint that replaces real email.

`edgespark dev` runs your full-stack EdgeSpark project on your machine. The CLI launches a local Miniflare process with both the user-worker and the platform's sidecar bound to local D1 and R2, starts the Vite dev server for the frontend, and proxies both under one origin with hot reload.

The goal is a zero-config loop for the common case — email/password auth, database, and storage all work immediately — and a short path to OAuth and external APIs when you need them.

## Start the dev server

From your project root:

```bash theme={null}
edgespark dev
```

By default the app is served at `http://localhost:7775`. Pick a different port with `--port`:

```bash theme={null}
edgespark dev --port 8080
```

`dev` is long-running. Stop it with `Ctrl+C`; stop and wipe local state in one step with `--reset`:

```bash theme={null}
edgespark dev --reset
```

## What runs locally

When `edgespark dev` starts, you get:

* **Local D1** — a SQLite file managed by Miniflare. The schema in `server/src/defs/db_schema.ts` is pushed via `drizzle-kit push` before the workers start, and re-pushed after every save; destructive changes (drop column, `NOT NULL` without a default) auto-apply and may truncate tables.
* **Local R2** — a filesystem-backed bucket. Presigned URL uploads work through a local S3 proxy.
* **Local auth** — email/password ready out of the box. Sessions use a local KV namespace.
* **Reverse proxy on one port** — `http://localhost:7775/api/*` hits the worker; every other path hits the Vite dev server. Same origin, so cookies and CORS are not a problem.
* **Hot reload** — backend source changes trigger an esbuild rebuild and reload the worker without a process restart. Frontend uses Vite HMR.
* **Dev-mode email** — no email is sent. Verification URLs are stored in a dev-only row keyed by email, and `/api/_es/dev/auto-verify` replays them through Better Auth so signup proceeds without a mailbox round-trip. To test real email delivery, deploy to a cloud environment.

Zero configuration is needed for any of this. Signup, session cookies, database queries, storage uploads, and the dev-only auto-verify helper all work against local bindings.

## Local values with `.env.local`

`.env.local` holds the values that differ between your machine and a deployed environment — OAuth client secrets, external API keys, and anything else declared in `server/src/defs/runtime.ts`.

The file is local to your machine, must stay gitignored, and is read at each `edgespark dev` start. On reload, changing `.env.local` is enough — the CLI re-reads values without you restarting the process.

Example:

```bash .env.local theme={null}
# Google OAuth — add http://localhost:7775/api/_es/auth/callback/google
# as a redirect URI in your Google OAuth app
GOOGLE_CLIENT_ID=123-xxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx

# External APIs — use test keys locally
STRIPE_SECRET_KEY=sk_test_...
```

Only keys declared in `server/src/defs/runtime.ts` (`VarKey` / `SecretKey` unions) or referenced by `configs/auth-config.yaml` are loaded into the local worker — undeclared lines in `.env.local` are silently ignored. Add the key to `runtime.ts` first.

<Warning>
  `.env.local` values live only on your machine. Never commit it, and do not reuse production keys locally. For deployed environments use `edgespark var set` and `edgespark secret set` instead — see [manage vars](/guides/variables) and [manage secrets](/guides/secrets).
</Warning>

Keys you declare in `server/src/defs/runtime.ts` but leave unset in `.env.local` show a startup warning and become `null` or `undefined` at runtime:

```text theme={null}
⚠ GOOGLE_CLIENT_SECRET declared in runtime.ts but not in .env.local
  → Google sign-in is unavailable locally until configured
```

OAuth providers enabled in `configs/auth-config.yaml` that reference a missing secret remain unavailable locally — the rest of the app boots normally.

## Dev-mode email and auto-verify

In `edgespark dev`, no email leaves your machine — the local sidecar always intercepts Better Auth's email hooks regardless of any `*_API_KEY` you set. Each verification URL Better Auth generates is written to a dev-owned row keyed by email, and the dev-only endpoint `/api/_es/dev/auto-verify` replays it through the auth handler to issue a session.

You normally do not call this endpoint by hand. `ctx.auth.createUser` (see [seed data](#seed-data-with-server-dev-seed-ts)) already calls it for you, and your app's signup flow can follow the same pattern in dev. The fact a URL was issued is logged so you can spot-check it:

```text theme={null}
[edgespark] Verification URL for alice@example.com — auto-verify via /api/_es/dev/auto-verify
```

If you need to test real email delivery (Resend, Postmark, etc.), deploy to a cloud environment. There is no `.env.local` switch that turns the dev override off.

## Hot reload behavior

| Change                          | Effect                                                                                                                               |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Backend source in `server/src/` | esbuild rebuilds the bundle; Miniflare reloads the worker in place                                                                   |
| Frontend source in `web/src/`   | Vite HMR updates the browser without a full reload                                                                                   |
| `.env.local`                    | Worker reloads with updated bindings                                                                                                 |
| `configs/auth-config.yaml`      | Worker reloads with the new auth config                                                                                              |
| `server/src/defs/db_schema.ts`  | Re-pushed via `drizzle-kit push` after every successful rebuild — saving the file is enough. Destructive changes auto-apply locally. |

Local data is ephemeral. Destructive schema changes — dropping a column, adding `NOT NULL` without a default — auto-apply locally and may truncate tables. Use [seed data](#seed-data-with-server-dev-seed-ts) so you can rebuild a known state on demand.

## Database and storage in dev

Both `client.db` and `client.storage` work against local bindings with no code changes:

* **Schema sync** — the CLI runs `drizzle-kit push` against local D1 before the workers accept traffic, and again after every backend rebuild, applying `server/src/defs/db_schema.ts` directly. No migration files are needed locally — the production deploy still runs `edgespark db migrate`.
* **D1 queries** — identical API to production; SQL validation runs in the sidecar the same way.
* **R2 uploads and downloads** — stored under `.edgespark/state/r2/`.
* **Presigned URLs** — the sidecar signs URLs against a local S3 proxy. Uploading to a presigned URL writes to the local bucket.

See [use the database](/guides/database) and [use storage](/guides/storage) for the SDK patterns; they do not change between local and deployed modes.

## Seed data with `server/dev/seed.ts`

Templates scaffolded by `edgespark init` include `@edgespark/devkit` in `server/package.json`. Create `server/dev/seed.ts` to populate your local database on every dev start:

```typescript server/dev/seed.ts theme={null}
import { defineSeed } from "@edgespark/devkit";
import type { SqliteRemoteDatabase } from "drizzle-orm/sqlite-proxy";
import { posts } from "../src/defs/db_schema";
import type * as schema from "../src/defs/db_schema";

export default defineSeed<SqliteRemoteDatabase<typeof schema>>(async (ctx) => {
  const { user, fetch } = await ctx.auth.createUser({
    email: "alice@example.com",
    password: "correct-horse-battery-staple",
    name: "Alice",
  });

  await ctx.db.insert(posts).values([
    { title: "Hello world", authorId: user.id },
    { title: "Second post", authorId: user.id },
  ]);

  const response = await fetch("/api/posts");
  console.log(`Seeded ${user.email} with ${(await response.json()).length} posts`);
});
```

The `ctx` the CLI passes in gives you:

| Field                                            | What it is                                                                                                                                                                                                                                                                                                                                                                             |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ctx.db`                                         | A Drizzle client bound to the local D1 database. Type it with your own `schema` for full inference.                                                                                                                                                                                                                                                                                    |
| `ctx.origin`                                     | The dev proxy origin, e.g. `http://localhost:7775`.                                                                                                                                                                                                                                                                                                                                    |
| `ctx.fetch(input, init?)`                        | `fetch` that resolves relative paths against `ctx.origin`. Absolute URLs pass through. Unauthenticated — use the `fetch` returned from `ctx.auth.createUser` for authenticated requests.                                                                                                                                                                                               |
| `ctx.auth.createUser({ email, password, name })` | Signs up a user through the real Better Auth endpoint, then auto-verifies them via the dev-only endpoint. Returns `{ user, fetch }` where `fetch` replays the user's session cookie on same-origin requests. If the project has email verification disabled, the auto-verify call returns 404 and the seed proceeds with the session cookie from sign-up — this is fine, not an error. |

The seed runs once when the dev server starts. It re-runs on the next `edgespark dev` if the file has changed or you pass `--reset`; edits during an already-running session take effect on the next start.

The change-detection sentinel is a SHA-256 of `server/dev/seed.ts` itself — it does not cover any helper modules `seed.ts` imports. If you split seed logic across files and edit only a helper, touch `seed.ts` (or pass `--reset`) to force a re-run.

<Note>
  `dev/seed.ts` runs only in `edgespark dev`. It is never bundled into a deployed build. Use it for reproducible local state, not for production data loading.
</Note>

## Local state and logs

Local persistence lives under your project:

```text theme={null}
.edgespark/
├── state/
│   ├── d1/          # SQLite files (survive restarts)
│   ├── r2/          # Stored objects (survive restarts)
│   ├── kv/          # KV data (sessions, auth)
│   └── secrets.json # Auto-generated dev-only internals
└── logs/
    ├── dev.log      # Current session
    └── dev.log.prev # Previous session (rotated at 5 MB)
```

`--reset` deletes the entire `.edgespark/state/` directory before starting. Use it whenever you want to rerun migrations and seed data against an empty database.

Tail `.edgespark/logs/dev.log` in a second terminal if you want a persistent log alongside the console output. Both `.edgespark/` and `.env.local` must stay gitignored.

## OAuth providers locally

To test a real OAuth sign-in against local dev, register a second OAuth app (or add a second callback URL on your existing one):

```text theme={null}
http://localhost:7775/api/_es/auth/callback/<provider>
```

Fill in the provider's client ID and client secret in `.env.local` using the exact key names from [add social login](/guides/social-login) (`GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`, etc.). Save the file — the worker auto-reloads with the new bindings. No platform-side `edgespark var set` or `edgespark secret set` is needed for local use; those commands target deployed environments.

## Troubleshooting

| Symptom                                                             | Likely cause                                                                                            | Fix                                                                                                                                                                                     |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Port 7775 already in use                                            | Another `edgespark dev` or process is on the port                                                       | Run `edgespark dev --port <n>` or stop the other process.                                                                                                                               |
| No verification email reaches inbox                                 | Local dev never sends real email — the auto-verify endpoint replays the URL through Better Auth instead | Use `ctx.auth.createUser` in `dev/seed.ts`, or POST to `/api/_es/dev/auto-verify?email=<email>` from your client-side flow. To test real email delivery, deploy to a cloud environment. |
| `edgespark dev` startup is stuck at "Running server/dev/seed.ts..." | A request inside `seed.ts` is hanging                                                                   | `ctx.fetch` and `ctx.auth.createUser` time out after 30 seconds and surface the error. Check the seed for a call to an unreachable external service.                                    |
| Stale data after schema changes                                     | Local destructive changes auto-applied and may have truncated tables                                    | Run `edgespark dev --reset` and let your `dev/seed.ts` rebuild state.                                                                                                                   |
| Provider button missing in local login UI                           | `.env.local` is missing a `*_CLIENT_SECRET`, or the provider is disabled in `configs/auth-config.yaml`  | Add the secret, confirm the provider block has `enabled: true`. Saving either file auto-reloads the worker.                                                                             |
| Backend changes not picked up                                       | esbuild rebuild error, or (in fullstack mode) a backend route is declared outside `/api/*`              | Read the error in the dev output; fix the build error or move the route under `/api/*`, then save again. Typecheck failures only print warnings — they never block reload.              |

For platform-wide naming and quota limits, see [platform limits](/reference/limits).

## See also

<Columns cols={2}>
  <Card title="Development workflow" icon="workflow" href="/guides/development-workflow">
    The edit → CLI → deploy loop for schema, storage, vars, secrets, and types.
  </Card>

  <Card title="Add social login" icon="lock" href="/guides/social-login">
    Register OAuth apps and wire providers for both local `dev` and deployed environments.
  </Card>
</Columns>
