Skip to content

justscale/justscale

Repository files navigation

JustScale

The TypeScript backend framework where plain code just scales.

Documentation →  ·  Philosophy  ·  Quick Start  ·  Why it scales

npm License: MIT Node

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.

What's in the box

  • Services & DIdefineService with function-based injection (no decorators, no reflect-metadata); missing dependencies fail the build, not the prod node.
  • ID-free domainRef<T> / Persistent<T> / Locked<T> flow through your code; storage owns IDs.
  • Safe mutationsrepo.update / save / delete require Locked<T>. The only way to obtain one is using 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 next as 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 processescreateProcess workflows written as plain async code that survive restarts and route across instances.
  • Custom TS compiler (ptsc) — process transforms + IDE support.

Authorization lives on the model

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.

Install

npx create-justscale my-app
cd my-app
just dev

The 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/.

Packages

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)

Requirements

  • 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.

Documentation

justscale.sh — guides, concepts, reference, and the visual explainer.

Development

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

License

MIT

Packages

 
 
 

Contributors

Languages