A production-ready monorepo starter with SvelteKit, Hono, oRPC, D1 database, and type-safe everything.
āāā packages/
ā āāā shared/ # Zod schemas, types, utilities
ā ā āāā src/schemas/ # Source of truth for all types
ā ā
ā āāā db/ # Database layer
ā āāā src/
ā āāā schema/ # Drizzle table definitions
ā āāā repositories/# Data access interfaces
ā āāā services/ # Business logic
ā āāā adapters/ # D1 implementation
ā
āāā apps/web/ # SvelteKit app
āāā src/
āāā lib/
ā āāā rpc.ts # Type-safe client
ā āāā server/rpc/ # Hono + oRPC
ā āāā context.ts # Request context
ā āāā procedures.ts # Base procedures
ā āāā router.ts # Main router
ā āāā handler.ts # Hono app + oRPC
ā āāā routers/ # Domain routers
ā āāā auth.ts
ā āāā posts.ts
āāā routes/
āāā api/[...paths]/ # All API requests
# Install dependencies
bun install
# Create D1 database
bunx wrangler d1 create starter-db
# Copy the database_id to apps/web/wrangler.toml
# Generate migrations
bun run db:generate
# Apply migrations locally
cd apps/web
bunx wrangler d1 migrations apply starter-db --local
# Start development server (with D1 bindings)
bun run preview
# Or start without bindings (for UI work)
bun run dev
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā SvelteKit Pages ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Hono ā
ā āāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāā ā
ā ā Middleware ā ā /api/rpc ā ā /api/health ā ā
ā ā (cors,log) ā ā (oRPC) ā ā (REST) ā ā
ā āāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Services ā
ā (Business logic layer) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Repositories ā
ā (Data access layer) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā D1 / MySQL / Postgres ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Key insight: Hono is the HTTP layer, oRPC is mounted on it. This gives you:
// lib/server/rpc/routers/posts.ts
import { publicProcedure, protectedProcedure } from '../procedures';
export const postsRouter = {
// Public - anyone can call
list: publicProcedure
.input(paginationSchema)
.output(postsResponseSchema)
.handler(async ({ input, context }) => {
const postService = new PostService(context.db);
return postService.getPublishedPosts(input);
}),
// Protected - requires authentication
create: protectedProcedure
.input(createPostSchema)
.output(postSchema)
.handler(async ({ input, context }) => {
// context.user is guaranteed non-null
const postService = new PostService(context.db);
return postService.createPost(context.user.id, input);
}),
};
// In +page.server.ts (SSR)
import { createClient } from '$lib/rpc';
export const load = async ({ fetch }) => {
const rpc = createClient(fetch);
const result = await rpc.posts.list({ limit: 10 });
return { posts: result.items }; // Fully typed!
};
<!-- In +page.svelte (client-side) -->
<script lang="ts">
import { rpc } from '$lib/rpc';
async function createPost() {
const post = await rpc.posts.create({
title: 'Hello',
content: 'World',
});
// post is fully typed as Post
}
</script>
Need a webhook or REST endpoint? Add it directly to the Hono app:
// lib/server/rpc/handler.ts
export function createApp(locals: App.Locals) {
const app = new Hono<Env>().basePath('/api');
// ... existing setup ...
// Add REST endpoints alongside oRPC
app.post('/webhooks/stripe', async (c) => {
const payload = await c.req.text();
// Handle webhook...
return c.json({ received: true });
});
// oRPC is mounted here
app.all('/rpc/*', async (c) => { ... });
return app;
}
| Endpoint | Description |
|---|---|
POST /api/rpc/auth.signUp |
Register new user |
POST /api/rpc/auth.signIn |
Login |
POST /api/rpc/auth.signOut |
Logout |
POST /api/rpc/auth.me |
Get current user |
POST /api/rpc/posts.list |
List published posts |
POST /api/rpc/posts.listMine |
List my posts |
POST /api/rpc/posts.create |
Create post |
POST /api/rpc/posts.update |
Update post |
POST /api/rpc/posts.delete |
Delete post |
GET /api/openapi |
OpenAPI spec |
GET /api/health |
Health check |
// lib/server/rpc/procedures.ts
// Base procedure with context
const o = os.$context<Context>();
// Public - anyone can call
export const publicProcedure = o;
// Protected - requires authenticated user
export const protectedProcedure = o.use(async ({ context, next }) => {
if (!context.user) {
throw new Error('UNAUTHORIZED');
}
return next({ context }); // context.user now guaranteed
});
// Admin - requires admin role
export const adminProcedure = protectedProcedure.use(async ({ context, next }) => {
if (context.user.role !== 'admin') {
throw new Error('FORBIDDEN');
}
return next({ context });
});
# Development
bun run dev # Start dev server
bun run check # Type check all packages
# Database
bun run db:generate # Generate migrations from schema
bun run db:migrate # Apply migrations
# Production
bun run build # Build for production
bun run preview # Build + run with wrangler dev
# Deploy to Cloudflare Workers
cd apps/web && bun run deploy
packages/shared/src/schemas/product.tspackages/db/src/schema/products.tspackages/db/src/repositories/interfaces.tspackages/db/src/adapters/d1.tspackages/db/src/services/product.service.tsapps/web/src/lib/server/rpc/routers/products.tsapps/web/src/lib/server/rpc/router.tsbun run db:generateMIT