Introduction
A TypeScript framework where domain code describes what happens, not how.
The Problem
Modern backend development has an infrastructure obsession. We spend more time wiring databases, managing connections, handling transactions, configuring authentication, and dealing with deployment than writing actual business logic.
Look at any backend codebase. The core logic โ what the application actually does โ is a small fraction of the code. The rest is infrastructure: how data gets stored, how services communicate, how errors propagate, how state survives restarts.
We've accepted this as normal. It isn't.
The T-Shirt Test
There's a running joke about programmer t-shirts with silly pseudocode:
while alive:
if not coffee:
get_coffee()
drink_coffee()
work()We laugh because "real code doesn't work like this." But why doesn't it? The logic is clear. The intent is obvious. We understand it instantly.
Now consider a poker game. The rules are simple: deal cards, bet in rounds, reveal the winner. Here's what that looks like in JustScale:
// Pre-flop
await bettingRound('preflop');
const afterPreflop = lastStanding();
if (afterPreflop) return afterPreflop;
// Flop
deck.pop(); // burn
exports.communityCards.push(deck.pop()!, deck.pop()!, deck.pop()!);
await bettingRound('flop');
const afterFlop = lastStanding();
if (afterFlop) return afterFlop;
// Turn
deck.pop(); // burn
exports.communityCards.push(deck.pop()!);
await bettingRound('turn');
const afterTurn = lastStanding();
if (afterTurn) return afterTurn;
// River
deck.pop(); // burn
exports.communityCards.push(deck.pop()!);
await bettingRound('river');
// Showdown
const winner = determineWinner(communityCards, remaining);Tip
bettingRound suspends the process while waiting for player actions โ for minutes, hours, or until a timeout. The compiler transforms it into a resumable state machine.JustScale's goal: make the t-shirt code real. Your domain logic should read like a description of what happens, not a manual for how infrastructure makes it happen.
What Makes JustScale Different
Durable Processes as Plain Code
Long-running workflows โ subscriptions, order fulfillment, game sessions โ are written as plain TypeScript. The compiler transforms them into state machines that persist to storage and resume after restarts.
const r = race();
switch (true) {
case signal(r, signals.paymentConfirmed):
return { status: 'paid', txId: r.txId };
case delay.days(r, 3):
return { status: 'timeout' };
}ID-Free Domain
An ID is an infrastructure detail โ how your storage tracks entities. In JustScale, domain code never sees string IDs. Persistent entities are references themselves.
// Pass entities directly โ no .id needed
await transfer(fromAccount, toAccount, amount);
// At boundaries, convert strings to typed refs
const user = User.ref`${userId}`;Type States as Contracts
The shape of your data tells you what you can do with it. A method that needs a locked entity says so in its signature โ and the compiler enforces it.
async markPaid(this: Lock<Persistent<Order>>) {
this.status = 'paid'; // Lock removes readonly โ safe to mutate
}
async loadItems(this: Persistent<Order>) {
return this.payments.getItemsFor(this); // Read-only access
}Transport Agnostic
Controllers are entry points, not HTTP handlers. The same business logic works with HTTP, WebSocket, CLI, gRPC, or events โ same DI, same middleware, same guards.
import { createController } from '@justscale/core';
import { Ws } from '@justscale/websocket/builder';
import { PokerService } from './poker.service.js';
export const PokerController = createController('/poker', {
inject: { poker: PokerService },
routes: ({ poker }) => ({
table: Ws('/:tableId')
.message(Command)
.handle(async ({ messages, send, params }) => {
await poker.openTable(params.tableId);
for await (const msg of messages) {
switch (msg.type) {
case 'sit':
await poker.sitDown(params.tableId, msg.playerId, msg.seatNumber, msg.buyIn);
break;
case 'action':
await poker.playerAction(msg.gameId, msg.playerId, msg.action, msg.amount);
break;
}
}
}),
}),
});How It All Fits Together
JustScale is a full-stack backend framework with compile-time type safety. No decorators, no runtime reflection โ plain TypeScript functions and objects. What you write is what runs.
- Dependency Injection โ type-safe, compile-time validated. Missing dependencies are caught before your code runs.
- Models โ domain entities with type-safe field builders, references, and methods that declare their data requirements.
- Repositories โ abstract storage. Swap PostgreSQL for in-memory without touching domain code.
- Processes โ durable workflows that survive restarts, with signals, race conditions, and timeouts.
- Features โ composable modules bundling services, controllers, and configuration.