| name | lunora-setup-auth |
| description | Adds authentication to a Lunora app. Use for sign-up/sign-in, email/password, OAuth (Clerk, Auth0), magic link, or email OTP via `lunora registry add auth`, wiring the auth handler into the Worker, and gating functions on the session. |
Lunora Setup Auth
Wire authentication into a Lunora app using the auth registry item, which is
built on @lunora/auth (a thin wrapper over
better-auth) with sessions persisted in
SessionDO and identity tables in D1.
When to Use
- Adding sign-up / sign-in to a Lunora app.
- Adding an OAuth/OIDC provider (Clerk, Auth0), magic link, or email OTP.
- Gating queries/mutations on the signed-in user.
When Not to Use
- The project has no Lunora backend yet — use
lunora-quickstart first.
- You only need to read
ctx.auth.userId in a function and auth is already
installed — just use it.
Workflow
- Add the base
auth item.
- Mount the auth request handler in the Worker entry.
- Configure env vars and the D1 database.
- (Optional) Layer a provider item (Clerk / Auth0 / magic link / OTP) on top.
- Gate functions on
ctx.auth.userId; gate UI with the auth gates/hooks.
Step 1: Add the base item
lunora registry add auth
This:
- Adds
@lunora/auth, @lunora/mail, and @lunora/server to package.json
(run pnpm install afterwards).
- Copies
lunora/auth/index.ts — the auth instance (buildAuth / getAuth)
and the /api/auth/* request handler (mountAuth) — into your project. It is
yours to edit.
- Adds a D1
DB binding to wrangler.jsonc (better-auth persists
users/sessions there).
- Scaffolds
BETTER_AUTH_SECRET, BETTER_AUTH_URL, and MAIL_FROM into
.dev.vars.
Step 2: Mount the handler
In your Worker entry, route /api/auth/* to the scaffolded handler (see the
generated lunora/auth/index.ts README block). createWorker handles the rest
of the RPC surface; the auth handler owns the better-auth endpoints.
Step 3: Env vars and the D1 database
| Var | Secret | Notes |
|---|
BETTER_AUTH_SECRET | yes | Encryption secret, min 32 chars. openssl rand -base64 32. |
BETTER_AUTH_URL | no | Public base URL, e.g. http://localhost:8787 in dev, your domain. |
MAIL_FROM | no | Sender for verification / reset mail. Captured in the dev Mail tab. |
Create the D1 database and paste its id into the DB binding in
wrangler.jsonc:
wrangler d1 create my-app-db
The better-auth schema (user/session/account/verification tables) is not
declared in lunora/schema.ts — it is managed by better-auth in D1. In dev,
ensureMigrated(auth) auto-applies it; in production prefer
compileMigrationsSql(auth.options) piped to wrangler d1 execute. Run
lunora doctor to confirm the DB binding has a real database_id (not a
placeholder).
Verification and password-reset emails are captured into the Lunora Studio
Mail tab in dev with zero email setup. For real delivery, lunora registry add mail (adds the SEND_EMAIL binding) or set RESEND_API_KEY.
Step 4: Add a provider (optional)
Each provider item builds on the base auth item (requires: ["auth"]):
lunora registry add auth-clerk
lunora registry add auth-auth0
lunora registry add auth-magic-link
lunora registry add auth-otp
Add the base auth item first (or let the registry resolve the requires
dependency). OAuth items need the provider's client id/secret added to
.dev.vars.
Step 5: Use the session
In functions
The runtime resolves the session and exposes the user on every context:
import { LunoraError, mutation, v } from "@lunora/server";
export const createDocument = mutation.input({ title: v.string() }).mutation(async ({ ctx, args: { title } }) => {
if (!ctx.auth.userId) {
throw new LunoraError("UNAUTHORIZED", "not signed in");
}
return ctx.db.insert("documents", { ownerId: ctx.auth.userId, title, createdAt: Date.now() });
});
For richer checks (org membership, roles), compose withAuthPlugins(auth) and
call the better-auth server API — see the scaffolded lunora/auth/index.ts.
In the UI (React)
import { Authenticated, Unauthenticated, useAuth } from "@lunora/react";
function Account() {
const { user, signIn, signOut } = useAuth();
return (
<>
<Authenticated>
<span>Signed in as {user?.email}</span>
<button type="button" onClick={() => signOut()}>
Sign out
</button>
</Authenticated>
<Unauthenticated>
<button type="button" onClick={() => signIn()}>
Sign in
</button>
</Unauthenticated>
</>
);
}
@lunora/react also exports AuthLoading and useAuthState for the loading
window before the session resolves.
Common Pitfalls
- Declaring better-auth tables in
lunora/schema.ts. They live in D1 and
are managed by better-auth — do not add them to defineSchema.
- Placeholder
database_id. The DB binding ships with a placeholder;
wrangler d1 create + paste the id, then lunora doctor to confirm.
- Missing/short
BETTER_AUTH_SECRET. better-auth needs ≥32 chars;
@lunora/auth surfaces a clear error when it is absent.
- Expecting prod email to "just work". Dev captures mail into the Studio;
production needs
mail (the SEND_EMAIL binding) or RESEND_API_KEY and a
verified sender domain.
Checklist