GuidesTypeScript

Elysia with Drizzle and Better Auth

Create an Elysia API on Bun with Drizzle, PostgreSQL, Better Auth, oRPC, and a TanStack Router frontend in one Better Fullstack workspace.

Updated October 9, 2026elysiabundrizzlebetter-auth

Use this stack when the API should be a separate Elysia service on Bun, with sign-in, typed procedures, and PostgreSQL access owned by the server.

npm create better-fullstack@latest my-elysia-api -- \  --ecosystem typescript \  --frontend tanstack-router \  --backend elysia \  --runtime bun \  --database postgres \  --orm drizzle \  --auth better-auth \  --api orpc \  --css-framework tailwind \  --ui-library shadcn-ui

What this creates

  • An Elysia server in apps/server that mounts Better Auth at /api/auth/*, oRPC at /rpc, and an OpenAPI reference at /api-reference.
  • A TanStack Router app in apps/web with sign-in and sign-up forms, a user menu, and a protected dashboard.
  • Drizzle on PostgreSQL through node-postgres, with the Better Auth tables and a task table.
  • Better Auth with email and password sign-in, using the Drizzle adapter.
  • Tailwind CSS and shadcn/ui components in the web app.
  • A Turborepo workspace and a reproducible bts.jsonc stack configuration.

Generated shape

txt
my-elysia-api/
  bts.jsonc
  turbo.json
  apps/
    server/
      .env
      src/index.ts
    web/
      .env
      src/
        lib/auth-client.ts
        utils/orpc.ts
        routes/
          index.tsx
          login.tsx
          dashboard.tsx
  packages/
    api/src/
      context.ts
      index.ts
      routers/index.ts
    auth/src/index.ts
    db/
      drizzle.config.ts
      src/
        schema/
          auth.ts
          task.ts
        tasks.ts
    env/src/
      server.ts
      web.ts

apps/server/src/index.ts is the only Elysia file. It applies CORS for CORS_ORIGIN with credentials, passes /api/auth/* requests to auth.handler, and hands /rpc and /api-reference requests to oRPC handlers built from the router in packages/api. The oRPC context reads the Better Auth session from the request headers.

Routes

RoutePurpose
GET /Returns OK.
/api/auth/*Better Auth endpoints for email sign-up, sign-in, session, and sign-out.
/rpc/healthCheckPublic oRPC procedure that returns "OK".
/rpc/privateDataProtected oRPC procedure. It throws UNAUTHORIZED without a session.
/api-referenceOpenAPI reference generated from the router.

The server listens on port 3000. The web app runs on Vite's port 5173 and calls the server through VITE_SERVER_URL.

Environment

FileVariablePurpose
apps/server/.envDATABASE_URLPostgreSQL connection string, postgresql://postgres:password@localhost:5432/postgres by default.
apps/server/.envBETTER_AUTH_SECRETGenerated random secret, at least 32 characters.
apps/server/.envBETTER_AUTH_URLPublic URL of the server, http://localhost:3000 locally.
apps/server/.envCORS_ORIGINWeb origin the server and Better Auth trust, http://localhost:5173 locally.
apps/web/.envVITE_SERVER_URLServer URL used by the auth client and the oRPC client.

packages/env/src/server.ts validates the server variables with Zod at startup, so a missing or short secret fails early.

Run it locally

Install Bun first: the server's dev script runs bun run --hot src/index.ts. Point DATABASE_URL at a PostgreSQL database, then install dependencies, push the Drizzle schema, and start both apps through Turborepo:

npm installnpm run db:pushnpm run dev

npm run dev:server and npm run dev:web start one app at a time. Open http://localhost:5173/login to create an account, then visit /dashboard, which calls privateData.

db:push applies the schema directly. For tracked migrations, use npm run db:generate and npm run db:migrate; Drizzle Kit writes them to packages/db/src/migrations.

Add task procedures

The generator writes task queries in packages/db/src/tasks.ts, scoped to a user ID, but the oRPC router does not expose them yet. Add a tasks entry to appRouter in packages/api/src/routers/index.ts, next to healthCheck and privateData:

ts
tasks: {
  list: protectedProcedure.handler(({ context }) => listTasks(context.session.user.id)),
  create: protectedProcedure
    .input(z.object({ title: z.string().trim().min(1) }))
    .handler(({ context, input }) => createTask(context.session.user.id, input.title)),
},

Import listTasks and createTask from @my-elysia-api/db/tasks and z from zod; the API package already depends on both. protectedProcedure rejects requests without a session, so every task query runs for the signed-in user only. The web app can then call orpc.tasks.list.queryOptions() with TanStack Query.

Compatibility notes

  • --runtime bun is the runtime for this Elysia server. The generated scripts call bun directly for dev, start, and compile.
  • oRPC is one choice here. The CLI also accepts --api trpc with Elysia and a React frontend.
  • Better Auth sets cookies with sameSite: "none" and secure: true in packages/auth/src/index.ts, because the web app and the API run on different origins.

Deployment notes

Build the server with npm run build (tsdown writes apps/server/dist) and start it with bun run dist/index.mjs, or produce a single binary with the server's compile script. Set DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL, and CORS_ORIGIN in the server environment. BETTER_AUTH_URL must be the public API origin and CORS_ORIGIN the exact web origin. Build the web app with VITE_SERVER_URL pointing at the deployed API, since Vite inlines it at build time.

Troubleshooting

SymptomCheck
The server exits on start with an env errorFill every variable in apps/server/.env; BETTER_AUTH_SECRET needs at least 32 characters.
Sign-up fails with a missing table errorRun npm run db:push against the database in DATABASE_URL.
Browser requests fail CORS checksCORS_ORIGIN must match the web origin exactly, including the port.
privateData returns UNAUTHORIZEDSign in first; the oRPC client sends cookies with credentials: "include".

Comparison notes

Choose Elysia when the team wants a Bun-first server with Elysia's plugin model. Compare Hono with Better Auth and Drizzle for a server that also runs on Node and Workers, or Fastify with Prisma and React for a Node server with Prisma.

Next steps

GitHub Sponsors