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.
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-uiWhat this creates
- An Elysia server in
apps/serverthat mounts Better Auth at/api/auth/*, oRPC at/rpc, and an OpenAPI reference at/api-reference. - A TanStack Router app in
apps/webwith 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 atasktable. - 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.jsoncstack configuration.
Generated shape
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.tsapps/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
| Route | Purpose |
|---|---|
GET / | Returns OK. |
/api/auth/* | Better Auth endpoints for email sign-up, sign-in, session, and sign-out. |
/rpc/healthCheck | Public oRPC procedure that returns "OK". |
/rpc/privateData | Protected oRPC procedure. It throws UNAUTHORIZED without a session. |
/api-reference | OpenAPI 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
| File | Variable | Purpose |
|---|---|---|
apps/server/.env | DATABASE_URL | PostgreSQL connection string, postgresql://postgres:password@localhost:5432/postgres by default. |
apps/server/.env | BETTER_AUTH_SECRET | Generated random secret, at least 32 characters. |
apps/server/.env | BETTER_AUTH_URL | Public URL of the server, http://localhost:3000 locally. |
apps/server/.env | CORS_ORIGIN | Web origin the server and Better Auth trust, http://localhost:5173 locally. |
apps/web/.env | VITE_SERVER_URL | Server 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 devnpm 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:
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 bunis the runtime for this Elysia server. The generated scripts callbundirectly fordev,start, andcompile.- oRPC is one choice here. The CLI also accepts
--api trpcwith Elysia and a React frontend. - Better Auth sets cookies with
sameSite: "none"andsecure: trueinpackages/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
| Symptom | Check |
|---|---|
| The server exits on start with an env error | Fill every variable in apps/server/.env; BETTER_AUTH_SECRET needs at least 32 characters. |
| Sign-up fails with a missing table error | Run npm run db:push against the database in DATABASE_URL. |
| Browser requests fail CORS checks | CORS_ORIGIN must match the web origin exactly, including the port. |
privateData returns UNAUTHORIZED | Sign 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
- Open the Stack Builder with Elysia selected.
- Read the Hono, Express, Fastify, and Elysia comparison.
- Review the CLI create reference.