A modern, type-safe full-stack boilerplate using Bun, Elysia, React Router, and Better Auth.
- β‘ Bun Runtime - Ultra-fast JavaScript runtime
- π¦ Elysia - Ergonomic web framework with end-to-end type safety
- π Better Auth - Modern authentication with session management, API keys, and OAuth
- ποΈ MikroORM - Type-safe ORM with SQLite support
- π @cryxto/ioc-n-di - Lightweight DI container with TypeScript decorators
- π OpenAPI - Auto-generated API documentation
- π― Access Control - Role-based permissions system
- π API Key Management - Built-in API key authentication
- βοΈ React 19 - Latest React with React Router v7
- π¨ Tailwind CSS - Utility-first CSS framework
- π Better Auth Client - Seamless authentication integration
- π TanStack Query - Powerful data fetching and caching
- π Shadcn UI - Beautiful, accessible component library
- π± Responsive Design - Mobile-first approach
- π¦ Bun Workspaces - Efficient monorepo management
- π Shared Contracts - Type-safe API contracts between frontend and backend
- π οΈ TypeScript - Full type safety across the stack
.
βββ apps/
β βββ backend/ # Elysia API server
β β βββ src/
β β β βββ main/ # Application entry point
β β β β βββ index.ts # Barrel exports
β β β β βββ types.ts # Type definitions
β β β β βββ factories.ts # App & server factories
β β β β βββ providers.ts # DI providers
β β β β βββ decorator.ts # DI decorators
β β β βββ modules/ # Feature modules
β β β βββ entities/ # Database entities
β β β βββ infrastructures/
β β β β βββ better-auth/ # Auth configuration
β β β β βββ config/ # Environment config
β β β β βββ orm/ # Database setup
β β β βββ common/ # Shared utilities
β β βββ public/ # Static assets
β β
β βββ frontend/ # React application
β βββ app/
β β βββ components/ # React components
β β βββ routes/ # React Router pages
β β βββ lib/
β β β βββ auth.ts # Better Auth client
β β β βββ api/ # API client functions
β β β βββ hooks/ # Custom React hooks
β β βββ root.tsx # App entry point
β βββ public/ # Static files
β
βββ packages/
βββ contracts/ # Shared TypeScript types
βββ src/
βββ index.ts # Exported types
- Bun >= 1.0.0
- Node.js >= 18 (for frontend tooling)
- Clone the repository:
git clone https://github.com/Cryxto/monorepo-elysia-rr
cd monorepo-elysia-rr- Install dependencies:
bun install- Set up environment variables:
Backend (apps/backend/.env):
DATABASE_URL=file:./dev.db
BETTER_AUTH_SECRET=your-secret-key-min-32-chars
BETTER_AUTH_URL=http://localhost:3000
FRONTEND_URL=http://localhost:5173
NODE_ENV=developmentFrontend (apps/frontend/.env):
VITE_API_URL=http://localhost:3000- Initialize the database:
bun mikro-rom schema:update --run --safeFrom the root directory:
# Start both backend and frontend
bun devOr start individually:
Backend:
cd apps/backend
bun devServer runs at: http://localhost:3000
Frontend:
cd apps/frontend
bun devApp runs at: http://localhost:5173
OpenAPI documentation available at:
The backend uses @cryxto/ioc-n-di - a lightweight dependency injection container with TypeScript decorators.
Key Principle: We import directly from @cryxto/ioc-n-di throughout the codebase - NO wrapper files or custom aliases. This keeps the architecture clean and transparent.
Container Setup (src/container.ts):
import 'reflect-metadata';
import { Container } from '@cryxto/ioc-n-di';
// Create and export singleton container instance
export const container = Container.createOrGet();Container Initialization (src/index.ts):
import { container } from '@container';
import { Provider } from '@cryxto/ioc-n-di';
// Bootstrap all providers
await container.bootstrap(providers);
// Get instances
const service = container.getInstanceOrThrow(MyService);Usage in Services:
import { Injectable } from '@cryxto/ioc-n-di';
import { InjectServer } from '@main/decorator';
import type { ApiServer } from '@main';
@Injectable()
export class MyController {
constructor(
@InjectServer() private server: ApiServer,
private myService: MyService, // Auto-injected by type
) {}
}Custom Decorators (src/main/decorator.ts):
// Custom decorators for common injections
import { Inject } from '@cryxto/ioc-n-di';
import { API_SERVER } from '../common/tokens';
export const InjectServer = () => Inject(API_SERVER);Key Principles:
- β
Import directly from
@cryxto/ioc-n-di- no wrappers - Container instance created once in
src/container.tsusingContainer.createOrGet() - Import container via
@containeralias throughout the app - Tokens defined in
common/tokens.tsto avoid circular dependencies - Custom decorators live in
main/decorator.ts - Services auto-injected by type (no explicit token needed)
Each module follows a consistent pattern:
modules/
βββ feature/
βββ feature.controller.ts # Route handlers
βββ feature.service.ts # Business logic
βββ feature.repository.ts # Data access
βββ index.ts # Module exports
Session-based Auth:
// Protected route
route.get('/profile',
async ({ user }) => ({ user }),
{ auth: true }
);Permission Checks:
beforeHandle: async ({ user }) => {
const hasPermission = await auth.instance.api.userHasPermission({
body: {
userId: user.id,
permission: { resource: ['action'] }
}
});
if (!hasPermission.success) {
throw new ForbiddenError('Insufficient permissions');
}
}API Key Auth:
// Create API key
const { data } = await authClient.apiKey.create({
name: 'my-api-key',
expiresIn: 86400, // 24 hours in seconds
});
// Use in requests
fetch('/api/resource', {
headers: {
'Authorization': `Bearer ${apiKey}`
}
});Better Auth client is configured in app/lib/auth.ts:
import { authClient, useSession } from '@/lib/auth';
// In components
function Profile() {
const { data: session } = useSession();
return <div>Welcome, {session?.user.name}</div>;
}
// Sign in
await authClient.signIn.email({
email: 'user@example.com',
password: 'password',
});import { useQuery } from '@tanstack/react-query';
function MyComponent() {
const { data, isLoading } = useQuery({
queryKey: ['myData'],
queryFn: async () => {
const res = await fetch('/api/my-data', {
credentials: 'include', // Include cookies
});
return res.json();
},
});
}// In routes
import { ProtectedRoute } from '@/components/protected-route';
export default function Dashboard() {
return (
<ProtectedRoute>
<DashboardContent />
</ProtectedRoute>
);
}bun dev- Start all servicesbun install- Install all dependencies
bun dev- Start dev server with watch modebun run migration:create- Create new migrationbun run migration:up- Run migrationsbun run migration:down- Rollback migrationsbun run seed- Seed database with sample data
bun dev- Start dev serverbun run build- Build for productionbun run preview- Preview production buildbun run lint- Run ESLint
- Create a new module:
cd apps/backend/src/modules
mkdir my-feature- Create service:
// my-feature/my-feature.service.ts
import { Injectable } from '@di';
@Injectable()
export class MyFeatureService {
async doSomething() {
// Your logic here
}
}- Create controller:
// my-feature/my-feature.controller.ts
import { Injectable } from '@di';
import { BaseController } from '@common/base.controller';
import { InjectServer } from '@main/decorator';
import type { ApiServer } from '@main';
@Injectable()
export class MyFeatureController extends BaseController {
constructor(
@InjectServer() server: ApiServer,
private myFeatureService: MyFeatureService,
) {
super(server);
}
protected applyRoutes(): void {
this.server.group('/my-feature', (route) =>
route.get('/', () => this.myFeatureService.doSomething())
);
}
}- Register in DI container:
// src/index.ts
import { MyFeatureController } from '@modules/my-feature/my-feature.controller';
import { MyFeatureService } from '@modules/my-feature/my-feature.service';
// Add to providers array
providers: [
// ...
MyFeatureService,
MyFeatureController,
]- Create component:
// app/components/my-component.tsx
export function MyComponent() {
return <div>My Component</div>;
}- Create route:
// app/routes/my-route.tsx
export default function MyRoute() {
return <MyComponent />;
}- Add to navigation (if needed):
// Update app/components/nav-main.tsx or similar- admin: Full access to all resources
- regular: Limited access (configurable per resource)
Defined per resource with actions:
create,read,update,deleteshare,revoke,inviteapprove,disapprove,reserve
Configure in apps/backend/src/infrastructures/better-auth/auth.ts:
- GitHub
- Discord
- Microsoft
# Create migration
bun run migration:create
# Run migrations
bun run migration:up
# Rollback
bun run migration:downDefine entities in apps/backend/src/entities/:
import { Entity, Property } from '@mikro-orm/core';
import { BaseEntity } from './base.entity';
@Entity()
export class MyEntity extends BaseEntity {
@Property()
name!: string;
}See DEPLOYMENT.md for detailed deployment instructions.
Quick Deploy to Fly.io:
./deploy.shThis uses a hybrid build strategy:
- Frontend built locally (avoids React 19 + Bun compatibility issues)
- Backend deployed with pre-built assets
- Zero-downtime deployments
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
MIT
- Bun - Fast JavaScript runtime
- Elysia - Web framework
- Better Auth - Authentication library
- React Router - React framework
- MikroORM - TypeScript ORM based on Unit of Work and Entity Mapper
- IoC-n-DI - Lightweight IoC Container and Dependency Injection
- Shadcn UI - UI components
For issues and questions:
- Open an issue on GitHub
- Check the documentation links above
Happy coding! π