The TypeScript backend framework where plain code just scales.
Documentation → · Philosophy · Quick Start · Why it scales
JustScale is a general-purpose TypeScript backend framework. You write plain, straight-line code and it just scales - from one instance to many.
The compiler makes long-running workflows durable, the type system keeps it correct, and domain code never sees a string ID. Like Go, where blocking code just scales - for TypeScript backends.
// A model is pure domain data - storage owns the id, your code never sees it.
export class Link extends defineModel({
name: 'Link',
fields: {
slug: field.string().unique(),
target: field.text()
},
}) {}
// A service is plain methods over injected dependencies - no transport, no SQL.
export class Links extends defineService({
inject: { links: ModelRepository.of(Link) },
factory: ({ links }) => ({
async shorten(slug: string, target: string) {
return links.insert({ slug, target });
},
async resolve(slug: string) {
return links.findOne(Link.fields.slug.eq(slug));
},
}),
}) {}
// A controller maps HTTP onto the service - the only place that knows about HTTP.
export const links = createController('/', {
inject: { svc: Links },
routes: ({ svc }) => ({
go: Get('/:slug').handle(async ({ params, res }) => res.json(await svc.resolve(params.slug))),
}),
});That's the whole app: a model, a service, a controller. The type system wires
the dependencies at compile time and catches mistakes - try to mutate without a
Locked<T> and it won't compile - and the same code runs unchanged from one
instance to many. Long-running work is just as plain; see durable processes
below.
- Services & DI —
defineServicewith function-based injection (no decorators, no reflect-metadata); missing dependencies fail the build, not the prod node. - ID-free domain —
Ref<T>/Persistent<T>/Locked<T>flow through your code; storage owns IDs. - Safe mutations —
repo.update/save/deleterequireLocked<T>. The only way to obtain one isusing x = await repo.lock(ref)— atomic with the read. - Transport-agnostic controllers — the same route definition is
served by HTTP, CLI, and Server-Sent Events today; WebSocket and gRPC
graduate from
nextas those packages settle. - Authorization on the model — declare
permit()rules next to the fields they guard;.guard(Model.can.edit)rejects unauthorized calls and one route can return a different shape per principal, all checked at compile time. - Durable processes —
createProcessworkflows written as plain async code that survive restarts and route across instances. - Custom TS compiler (
ptsc) — process transforms + IDE support.
Permission rules sit next to the fields they guard, then the controller enforces them at the edge. The same route can return a different shape depending on who is asking — and the compiler checks every branch.
import { permit, Everyone, permissions } from '@justscale/permission';
export class Post extends defineModel({
name: 'Post',
fields: { author: field.ref(Author), title: field.string(), body: field.text() },
// Rules live with the data. `.when(author)` ties "only the author" to the
// row's reference, so one rule drives route guards, response views, and
// filtered queries - declared once, reused everywhere it's enforced.
permissions: ({ author }) => ({
edit: permit(Author).when(author),
view: permit(Everyone).always(),
}),
}) {}
export const posts = createController('/', {
inject: { svc: Posts },
routes: ({ svc }) => ({
// 403 unless the caller satisfies Post.can.edit.
update: Put('/posts/:post')
.types({ Post })
.use(auth).use(permissions)
.guard(Post.can.edit)
.returns(204)
.handle(async ({ params, body }) => {
svc.edit(await params.post, body);
res.status(204);
}),
// One route, two response shapes - picked by which permission holds.
read: Get('/posts/:post')
.types({ Post })
.use(auth).use(permissions)
.returns(200, PostOwnerView, Post.can.edit)
.returns(200, PostPublicView, Post.can.view)
.handle(async ({ params, res }) => {
const post = await params.post;
if (res.permission === 'edit') {
return res.json({ title: post.title, body: post.body });
}
return res.json({ title: post.title });
}),
}),
});permit(Author) resolves the caller through your principal provider; Everyone
matches any caller. The full picture — filtered queries via byPermissions(),
explicit grants, field-level visibility — is in the
webshop and crowdfunding examples.
npx create-justscale my-app
cd my-app
just devThe installer detects your package manager and IDE, scaffolds a project
with an env-contract entrypoint (no main.ts, no manual app.serve()),
and ships JustScale-aware Claude Code skills under .claude/skills/.
This 0.x release ships the tier-1 surface. More packages
(websocket, event, redis, ...) graduate out of next
as their APIs settle.
| Package | Description |
|---|---|
@justscale/core |
DI, services, controllers, durable processes, models, cluster, CLI |
@justscale/typescript |
Custom TypeScript compiler (ptsc), tsserver, register hook |
@justscale/testing |
createTestKit harness, mocks, in-memory adapters |
@justscale/http |
HTTP route factories, body limits, CORS, OpenAPI hooks |
@justscale/sse |
Server-Sent Events route factory + streaming handlers |
@justscale/datastar |
Datastar reactive streaming — server-driven hypermedia over SSE |
@justscale/postgres |
Repositories, migrations, advisory locks, LISTEN/NOTIFY |
@justscale/auth |
User/Session models, password hashing, auth middleware |
@justscale/permission |
permit() rules on models, Model.can.* guards, permission-scoped responses |
@justscale/hmr |
Dev-only hot module reload — file watcher driving container.hotReload() |
create-justscale |
Project scaffolder (npx create-justscale my-app) |
- Node.js 24+
- pnpm 10.6+ (for development)
- PostgreSQL 16+ (for the postgres adapter)
Local dev runs against real Postgres via docker compose up -d.
pglite is for tests + CLI tooling, not for just dev.
justscale.sh — guides, concepts, reference, and the visual explainer.
- Introduction
- Philosophy — the nine principles
- Why it scales — the proof, not the slogan
git clone https://github.com/justscale/justscale.git
cd justscale
pnpm install
pnpm build # build all packages
pnpm test # run tests (needs docker pg)
pnpm lint # check linting
pnpm typecheck # workspace typecheck