A full-stack TypeScript backend framework with routing, ORM, queues, auth, real-time, and AI agents. All wired up. Conventions in place. Ready on day one.
Controllers, services, providers — all auto-resolved from the IoC container. Drop a file in the right folder, it just works.
Route → Controller → Service → Model. Every Laravel idiom maps 1:1 — Eloquent queries, FormRequests, Policies, Events. Type-safe end-to-end.
Built-in agents with tool schemas and streaming. Deploy to Node, Bun, Lambda, or Cloudflare Workers without rewriting a single route.
Same patterns. Same flow. TypeScript type safety on top.
// app/Http/Controllers/UserController.php
class UserController extends Controller
{
public function __construct(
private readonly UserService $users,
) {}
public function store(StoreUserRequest $request): JsonResponse
{
$user = $this->users->create($request->validated());
event(new UserRegistered($user));
return response()->json($user, 201);
}
}// app/controllers/UserController.ts
@Injectable()
export class UserController extends Controller {
constructor(private users: UserService) {
super()
}
async store(req: Request): Promise<Response> {
const user = await this.users.create(req.validated())
await event(new UserRegistered(user))
return this.json(user, 201)
}
}validated() pattern event() helper Click through a real request — from route definition all the way down to the database model.
import { Route } from '@faber-js/router'
import { PostController } from '../app/controllers/PostController'
Route.group({ prefix: '/api', middleware: ['auth'] }, () => {
Route.get('/posts', [PostController, 'index'])
Route.post('/posts', [PostController, 'store'])
Route.get('/posts/:id', [PostController, 'show'])
Route.put('/posts/:id', [PostController, 'update'])
Route.delete('/posts/:id', [PostController, 'destroy'])
})DI, typed tool schemas, per-session memory, authorization, structured output, and SSE streaming. Built into the framework — not bolted on.
import { Agent, Tool, Authorize, t } from '@faber-js/ai'
import { Order } from '../models/Order'
export class SupportAgent extends Agent {
override model = 'claude-sonnet-4-6'
override output = t.object({
summary: t.string(),
severity: t.enum(['low', 'medium', 'high']),
nextStep: t.string().optional(),
})
@Tool({
description: 'Look up an order by ID',
input: { id: t.string() },
})
@Authorize('view-order', ([{ id }]) => id)
async lookupOrder({ id }: { id: string }) {
return Order.with('items', 'shipment').findOrFail(id)
}
@Tool({
description: 'Refund an order — requires manager role',
input: { id: t.string(), reason: t.string() },
})
@Authorize('refund-order', ([{ id }]) => id)
async refund({ id, reason }) {
return await this.refundService.process(id, reason)
}
}Every feature a production backend requires — shipped as first-class, independently versioned packages.
Route groups, resource routes, named routes, and model binding — fluent API, zero config.
Route.group({ prefix: '/api', middleware: ['auth'] }, () => {
Route.resource('posts', PostController)
Route.get('/me', [UserController, 'me'])
})Eloquent-style models with relationships, scopes, and an expressive query builder. SQLite, PostgreSQL, MySQL — swap drivers with one line.
const posts = await Post
.where('published', true)
.with('author', 'tags')
.orderBy('created_at', 'desc')
.paginate(20)BullMQ-backed queues with a one-liner dispatch API. Retry, delay, prioritise. Or use the sync driver — no Redis required for local dev.
await dispatch(new SendWelcomeEmail(user))
await dispatch(new ProcessPayment(order))
.onQueue('payments')
.delay(60)Decouple your app with a typed event bus. Listeners can run synchronously or queued.
await event(new UserRegistered(user))
class SendWelcomeEmail {
async handle(e: UserRegistered) {
await Mail.to(e.user.email).send(new WelcomeMail())
}
}JWT guards, API tokens with scoped abilities, password reset, and resource policies — wired in automatically.
Route.group({ middleware: ['auth'] }, () => {
Route.get('/dashboard', [DashController, 'index'])
})
const user = req.user<User>()
await this.authorize('update', post)Generate controllers, models, jobs, mailables, agents, migrations and more from a single command.
npx faber make:model Post -m
npx faber make:agent SupportAgent
npx faber make:mail WelcomeMail
npx faber db:migrate
npx faber serveBuild React or Vue SPAs using server-side routing — no separate API, no manual JSON wiring. End-to-end type safety.
return this.render('Users/Index', {
users: await this.userService.all(),
})
const { props } = usePage<{ users: User[] }>()
<Link href="/users/create">Create</Link>WebSocket channels that feel like HTTP routes — public, private, presence — same DI, same auth, same shape.
Channel.presence('room.{slug}', [RoomChannel, 'join'])
async join(socket: Socket, slug: string) {
socket.joinPresence(`room.${slug}`, { id, name })
socket.on('msg', (m) => socket.broadcast(m))
}One declaration drives your model, migrations, validation rules, factory, and OpenAPI spec — all type-inferred.
const User = schema('users', {
id: t.id(),
name: t.string().min(2).max(100),
email: t.email().unique(),
role: t.enum(['admin','editor','viewer']),
})Zero-config request, query, and event tracing. Live at /_faber in dev — disabled in production automatically.
app.register(new DevToolsServiceProvider(app, {
db: getConnection(),
dispatcher: eventDispatcher,
}))
// Open http://localhost:3000/_faberRedis, memory, and database drivers with the same fluent API. Atomic locks and a built-in rate limiter.
await Cache.remember('users:active', 60, async () => {
return User.where('active', true).get()
})
await Cache.lock('process-order:1234', 10).get(async () => {
await processOrder(order)
})Pick the runtime that fits your deployment. Swap any time — the rest of your app doesn't change.
// server.ts — production Node deployment
import { app } from './bootstrap/app'
import { createFastifyAdapter } from '@faber-js/adapters/fastify'
const adapter = createFastifyAdapter(app)
await adapter.listen({ port: 3000, host: '0.0.0.0' })A full-featured CLI that generates anything your app needs. Modelled on Laravel's Artisan so every command feels familiar.
Every package is independently versioned and published under @faber-js. Install only what you need — or scaffold everything at once.
One command. Conventions in place. Production-ready from day one.