Our Philosophy
Superfunction is built on principles borrowed from Ruby on Rails, proven software practices, and modern TypeScript development.
Inspired by Rails
Ruby on Rails revolutionized web development with its philosophy. Superfunction brings similar principles to TypeScript and Next.js:
Convention over Configuration
Rails: File name determines route, model name determines table
ruby# app/controllers/posts_controller.rb class PostsController < ApplicationController def index @posts = Post.all end end
Superfunction: File-based routing, type-safe route definitions
typescript// src/server/routes/posts.ts import { route } from '@spfn/core/route'; export const getPosts = route.get('/posts') .handler(async (c) => { const posts = await db.query.posts.findMany(); return c.json({ posts }); });
Don't Repeat Yourself (DRY)
Rails: Define once, use everywhere (ActiveRecord models)
rubyclass User < ApplicationRecord validates :email, presence: true, uniqueness: true end # Validation, types, and database schema all from one definition
Superfunction: Route Definition as Single Source of Truth
typescriptimport { route } from '@spfn/core/route'; import { Type } from '@sinclair/typebox'; // Define once export const createUser = route.post('/users') .input({ body: Type.Object({ email: Type.String({ format: 'email' }) }) }) .handler(async (c) => { const { body } = await c.data(); const user = await create(users, body); return c.created(user); }); // ✅ TypeScript types inferred // ✅ Runtime validation automatic // ✅ API client generated (RPC-style) // ✅ OpenAPI docs generated
Optimize for Programmer Happiness
Rails: Make developers productive and joyful
Superfunction: Eliminate boilerplate, maximize productivity
typescript// ❌ Before: Manual typing, validation, client code interface CreateUserBody { email: string; } interface CreateUserResponse { id: number; email: string; } async function createUser(body: CreateUserBody): Promise<CreateUserResponse> { const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return response.json(); } // ✅ After: Route definition → Everything automatic const user = await api.users.create({ body: { email: 'user@example.com' } }); // Fully typed, validated, generated
The Omakase Stack
Rails: Curated tools that work together seamlessly
- ActiveRecord for database
- ActionView for templates
- ActiveJob for background jobs
Superfunction: Best-in-class TypeScript ecosystem
- PostgreSQL for database (25+ years proven)
- Next.js for frontend (industry standard)
- Hono for backend (modern, fast)
- TypeBox for validation (JSON Schema standard)
- Drizzle for ORM (type-safe SQL)
Omakase (おまかせ): Japanese for "I'll leave it up to you." Trust the chef to select the best ingredients and prepare the best meal.
Core Principles
1. Single Source of Truth
One route definition powers your entire stack:
typescript// src/server/routes/posts.ts import { route } from '@spfn/core/route'; import { Type } from '@sinclair/typebox'; export const createPost = route.post('/posts') .input({ body: Type.Object({ title: Type.String({ minLength: 1, maxLength: 200 }), content: Type.String() }) }) .handler(async (c) => { const { body } = await c.data(); const post = await create(posts, body); return c.created(post); });
From this one definition:
- ✅ TypeScript types inferred
- ✅ Runtime validation applied
- ✅ API client generated (RPC-style)
- ✅ OpenAPI documentation created
- ✅ Backend handler typed
- ✅ Frontend calls typed
- ✅ Component props typed
Types flow everywhere:
typescript// 1. Extract types from route (using AppRouter) import type { AppRouter } from '@/server/router'; import type { InferRouteOutput } from '@spfn/core/route'; export type Post = InferRouteOutput<AppRouter['posts']['create']>; // 2. Use in API calls const post = await api.posts.create({ title: 'Hello', content: '...' }); // post is fully typed // 3. Use in component props interface PostListProps { posts: Post[]; } function PostList({ posts }: PostListProps) { return posts.map(post => ( <PostCard key={post.id} post={post} /> )); } // 4. Use in nested components interface PostCardProps { post: Post; // Same type from route! } function PostCard({ post }: PostCardProps) { return <div>{post.title}</div>; }
Why it matters:
- No sync issues between types and validation
- Change once, update everywhere
- Impossible to have mismatched types
- Component props always match API responses
2. Proven Over Novel
Choose battle-tested technologies over trendy alternatives:
| Technology | Alternative | Why We Chose It |
|---|---|---|
| PostgreSQL | MySQL | 25+ years production, ACID, extensions |
| Next.js | Remix, Astro | Industry standard, proven at scale |
| Hono | Express, Fastify | Ultrafast, lightweight, zero dependencies |
| TypeBox | Zod, Yup | JSON Schema standard, 10x faster |
Philosophy:
- New isn't always better
- Stability matters in production
- Choose tools with longevity
3. Type Safety First
Catch errors at compile time, not production:
typescript// ❌ Runtime error (traditional approach) const user = await fetch('/api/users/abc').then(r => r.json()); console.log(user.id.toUpperCase()); // Runtime error: id is number! // ✅ Compile-time error (Superfunction) const user = await api.users.getById({ params: { id: 'abc' } }); // ^^^^^^^^ // TypeScript Error: Type 'string' is not assignable to type 'number'
Benefits:
- Errors caught during development
- Refactoring is safe
- IDE autocomplete everywhere
- No surprise runtime errors
4. Convention over Configuration
Sensible defaults, minimal configuration:
typescript// ❌ Other frameworks: Explicit configuration app.post('/users', validateBody(createUserSchema), authenticate, async (req, res) => { // Handler } ); // ✅ Superfunction: Convention-based export const createUser = route.post('/users') .input({ body: CreateUserSchema }) .use([Authenticated()]) .handler(async (c) => { // Validation automatic // Types automatic // Just write logic });
Conventions:
- Organize routes by domain:
src/server/routes/users.ts - Route naming:
getUser→api.users.get() - Router structure:
defineRouter({ users: { get: getUser } })
5. No Trade-offs
You shouldn't choose between performance and productivity:
Performance AND Developer Experience:
- TypeBox: Fast validation + type inference
- Hono: Fast routing + great DX
- Drizzle: Fast queries + type safety
Type Safety AND Speed:
- Compile-time type checking
- Runtime validation optimized
- Code generation at build time
Simplicity AND Power:
- Simple route definition syntax
- Powerful middleware system
- Advanced features available when needed
6. Integrated System
Everything works together seamlessly:
Terminalmy-app/ ├── src/ │ ├── app/ # Next.js frontend │ ├── server/ │ │ ├── routes/ # Route definitions │ │ └── router.ts # defineRouter │ └── lib/ │ └── api/ # Generated client
One project, complete stack:
- Frontend calls backend with type safety
- Backend validates with route input schemas
- Database queries with Drizzle ORM
- All TypeScript, all type-safe
7. Sharp Knives
Provide powerful tools, trust developers to use them responsibly:
Rails philosophy:
"We're adults. We can handle sharp knives."
Superfunction applies this: No artificial limitations, full control when needed.
Break Conventions When Needed
typescript// ✅ Recommended: Use define-route pattern export const getUser = route.get('/users/:id') .input({ params: Type.Object({ id: Type.String() }) }) .handler(async (c) => { const { params } = await c.data(); const user = await db.query.users.findFirst(...); return c.json(user); }); // ✅ Also allowed: Write raw Hono routes app.get('/custom-endpoint', async (c) => { // No validation, full Hono control return c.json({ custom: 'response' }); });
Database Freedom
typescriptimport { findOne, findMany, create } from '@spfn/core/db'; import { getDatabase } from '@spfn/core/db'; import { usersTable } from '@/server/db/schema'; // ✅ Recommended: Use helper functions (simple, type-safe) const user = await findOne(usersTable, { id: 1 }); const users = await findMany(usersTable, { where: { active: true }, limit: 10 }); const newUser = await create(usersTable, { email: 'user@example.com', name: 'User' }); // ✅ Also allowed: Direct Drizzle access (more power) const db = getDatabase('read'); const users = await db.query.users.findMany({ where: (users, { gt, and, eq }) => and( gt(users.createdAt, new Date('2024-01-01')), eq(users.active, true) ), with: { posts: true } }); // ✅ Maximum freedom: Raw SQL for complex queries const db = getDatabase('write'); const results = await db.execute(sql` WITH RECURSIVE category_tree AS ( SELECT id, name, parent_id, 1 as level FROM categories WHERE parent_id IS NULL UNION ALL SELECT c.id, c.name, c.parent_id, ct.level + 1 FROM categories c INNER JOIN category_tree ct ON c.parent_id = ct.id ) SELECT * FROM category_tree ORDER BY level, name `);
Deployment Freedom
Option 1: All-in-one deployment (Recommended)
Terminal# Docker (full control) docker-compose up # Provided docker-compose.yml with PostgreSQL # Railway, Render, Fly.io (easy deployment) git push # Automatic deployment with persistent process # Your own server (maximum control) pnpm run spfn:build # or: npx spfn build pnpm run spfn:start # or: npx spfn start
Why this works best:
- ✅ Persistent connection pooling (no cold starts)
- ✅ Single deployment, easier monitoring
- ✅ Simple CORS configuration
Option 2: Split deployment (Vercel + separate server)
Terminal# Next.js → Vercel (excellent Next.js hosting) # Superfunction server → Railway/Render/VPS # Build only server pnpm run spfn:build --server-only # Deploy separately vercel deploy # Next.js frontend railway up # Superfunction backend
When to use split deployment:
- ✅ You want Vercel's Next.js optimizations
- ✅ You need connection pooling for database
- ⚠️ Requires managing two services
- ⚠️ Need to configure CORS
No vendor lock-in:
- Standard Docker setup
- Works on any platform with persistent Node.js
- PostgreSQL anywhere (Supabase, Neon, self-hosted)
- No platform-specific code
Skip Safety When Needed
typescript// Skip middleware for specific endpoints (no .use() call) export const publicRoute = route.get('/public') .handler(async (c) => { // No authentication middleware applied return c.json({ public: true }); }); // Use any type when you need to const data: any = await externalAPI.fetch(); // Escape hatch
The philosophy:
- Conventions are defaults, not restrictions
- Type safety is recommended, not enforced
- Deploy anywhere, not just Vercel
- Use escape hatches when you need them
- We trust you to make the right choice
Design Decisions
Why File-Based Routing?
Rails convention: File location determines URL
Superfunction: Route definition determines URL, file structure is for organization
typescript// src/server/routes/users.ts export const getUsers = route.get('/users') // ← This defines the API route .handler(async (c) => { const users = await findMany(users); return c.json(users); }); // File location doesn't affect the route! // This file could be anywhere, route path determines the URL
Recommended file structure (for clarity, not enforcement):
Terminalsrc/server/routes/ ├── users.ts # Contains /users routes ├── posts.ts # Contains /posts routes └── router.ts # defineRouter({ users, posts })
Key difference:
- Rails: File location → URL (enforced)
- Superfunction: Route path → URL (enforced), File structure → Organization (recommended)
Why this matters:
- Route definition is the single source of truth for routing
- File structure helps developers find related routes quickly
- No magic file-to-URL mapping to remember
- Freedom to organize files however you want
Why Route-Definition-First?
API-first design:
- Define route with input schemas
- Frontend/backend implement independently
- Type inference ensures compatibility
Benefits:
- Parallel development
- Clear API boundaries
- Type safety guaranteed
Why Single Project?
Traditional approach: Separate repositories
frontend/(Next.js)backend/(Express/Hono)shared/(types)
Superfunction: Two servers, one project
- One
package.json - Two separate server processes (Next.js + Hono)
- Shared route definitions and types
Advantages:
- Lower cognitive load: Feels like one project, not three
- Traditional monorepo:
frontend/+backend/+shared/= 3 projects to think about - Superfunction: Just write code
- Traditional monorepo:
- Type sharing: Natural via route definitions (no package publishing)
- Single deployment: One build, one deployment
- Next.js-like DX: We're building tools to make backend development as easy as Next.js frontend
- Simple commands (
spfn dev,spfn build) - Auto-reload, auto-generation
- Convention over configuration
- Simple commands (
Trade-offs:
- Larger
node_modules(frontend deps + backend deps) - Frontend packages installed on server side
- Backend packages installed on frontend side
Note: We're actively working to improve this with better dependency separation.
Why it's worth it:
- Developer experience >>> disk space
- Significantly lower cognitive load
- No mental context switching between repositories
- Refactoring across stack is seamless
- One place to find everything
What Superfunction Is Not
Not a Framework
Superfunction is a tool built on existing frameworks:
- Uses Next.js (not replacing it)
- Uses Hono (not wrapping it)
- Enhances with type safety and generation
Not Opinionated About Everything
Opinionated:
- Schema validation (TypeBox)
- Database (PostgreSQL + Drizzle)
- Frontend (Next.js)
Unopinionated:
- UI library (use any React library)
- State management (use what you prefer)
- Styling (Tailwind, CSS-in-JS, anything)
- Authentication (implement your way)
Not Black Box Magic
Minimal automation, maximum transparency:
- Generated code is readable TypeScript: API client you can understand and debug
- Conventional defaults with escape hatches: Auto-handles server config, Drizzle setup
- Convention: Auto-generated configuration for DX
- Override: Write your own config when needed
- Standard frameworks: Next.js and Hono code, no proprietary runtime
- No hidden proxies: Direct function calls, not Proxy-based magic
What we auto-handle:
- Server setup and initialization
- Database connection configuration
- Route registration and middleware
- API client generation
What you can override:
- Custom server configuration
- Manual Drizzle setup
- Direct Hono route registration
- Custom API client code
We automate the boring stuff, but never hide what's happening.
Influenced By
Ruby on Rails
- Convention over configuration
- Programmer happiness
- Integrated system
tRPC
- End-to-end type safety
- Contract-based API
- Great developer experience
Prisma
- Type-safe database access
- Schema-first approach
- Auto-generated client
Next.js
- File-based routing
- Zero-config defaults
- Production-ready
Join the Philosophy
Superfunction is for developers who believe:
- ✅ Type safety prevents bugs
- ✅ Conventions reduce boilerplate
- ✅ Proven tools beat trendy ones
- ✅ Developer experience matters
- ✅ One source of truth eliminates sync issues
Get started:
Terminalnpx spfn@alpha create my-app cd my-app pnpm run spfn:dev # or: npm run spfn:dev
Next Steps:
Learn why we chose each technology in our stack.