С любовью ко всем разработчикам на свете ❤️
One command is the whole setup. It scaffolds a complete app — SSR, Prisma + SQLite, Tailwind, type-safe routing — installs dependencies, and runs codegen:
bun create point0-app@latest
# prompts for a name and a bundler (Bun or Vite), installs deps, runs setup
cd my-app
bun run dev # http://localhost:3001 (client) + http://localhost:3000 (server)That's it — the app is running. You need Bun; the scaffolder offers to install it if it's missing.
The home page ships a small playground: an Idea Prisma model, a query that
lists the rows, and a mutation with a form that adds one — setup seeds a few
ideas, so there's something to poke at right away.
The rest of the page is optional depth: the scaffolder, the commands you'll run every day, and manual setup — every file the scaffolder writes, for when you'd rather wire the app by hand.
bun create point0-app resolves to the create-point0-app package by Bun's
create-<x> convention. With no arguments it prompts interactively: project
name, bundler (Bun or Vite, default Bun), install now (default yes), and
override (asked only when the target directory is non-empty). Pass a name and
flags to skip the prompts:
bun create point0-app@latest my-app # name as a positional argument
bun create point0-app@latest my-app --vite # use Vite for the client bundle
bun create point0-app@latest my-app --no-install # scaffold only, don't install
bun create point0-app@latest my-app -I # non-interactive, take all defaultsIt scaffolds into <cwd>/<name>. If that directory already exists and is not
empty, the run stops unless you pass --override (or confirm the prompt).
| Flag | Default | What it does |
|---|---|---|
[name] | my-app | Directory name for the new app |
--vite / --no-vite | --no-vite (Bun) | Bundle the client with Vite instead of Bun |
--install / --no-install | --install | Run bun install + bun run setup after scaffolding |
-O, --override | off | Allow writing into a non-empty target directory |
-I, --no-interactive | off | Skip every prompt and use the defaults above |
The scaffolder checks for Bun first. With -I and no Bun on PATH, it errors
out (Bun is required but was not found in PATH). Interactively, it offers to
install Bun globally with npm install -g bun before continuing.
bun create point0-app is the only supported entry point — there's no npx /
pnpm dlx / yarn create equivalent. Those would resolve the
create-point0-app bin, but both the scaffolder and the app it writes run on
Bun, so they'd dead-end the moment you tried to use the result.
Point0 never loads TypeScript. dev, build and generate don't touch the
compiler API: the Point0 compiler parses your code with Babel and reads the
paths aliases out of tsconfig.json itself. So the TypeScript in your project
is yours alone — 6, 7, or none at all, the framework behaves identically.
The scaffold pins TypeScript 7, and bun run types runs its tsc.
Staying on 6 is a normal choice, and one tool makes it the right one:
typescript-eslint's type-aware linting doesn't run on TypeScript 7 — 7.0
ships no compiler API, and typescript-eslint needs one. Support is waiting on
the stable API in 7.1. If you lint with projectService or project, keep
"typescript": "^6.0.3" and add the new compiler under a second name:
{
"devDependencies": {
"typescript": "^6.0.3",
"@typescript/native": "npm:typescript@^7.0.2"
}
}tsc then resolves to TypeScript 7 while import 'typescript' still hands
typescript-eslint the API it needs. This is exactly how the Point0 repo itself
is set up. Don't alias the typescript name to @typescript/typescript6 —
under Bun the alias resolves back into itself and you end up with an empty API.
The --vite choice changes which files you get:
bun-plugin-tailwind, declared as bunPlugins in engine.ts. No
vite.config.ts.viteConfig block in engine.ts and
a vite.config.ts; CSS is linked from index.html; preload.ts gets
preventLoadBunPlugins: true.You can't have both — engine.ts carries viteConfig or bunPlugins, never
both. The trade-offs are in bun-vs-vite; the Vite walkthrough is
vite.
Four scripts you'll live in:
bun run dev # point0 dev --hot — dev server + client, server hot reload
bun run setup # migrate the SQLite DB, generate Prisma + point0 code, seed
bun run build # point0 build → dist/
bun run start # NODE_ENV=production bun run ./dist/server/index.server.jsWith --install (the default) the scaffolder already ran bun install and
bun run setup, so bun run dev works right away. Run the chain yourself after
--no-install, a fresh clone, or a database reset:
bun install
bun run setup
bun run devbun run setup is the important one. src/generated/ (the Prisma client and
point0's points / routes / assets) is gitignored — setup produces it.
Without it, typecheck fails and the app won't run.
Why
setup, notprepare? Apreparescript runs on everybun install— which breaks installs where no database exists yet (CI, a monorepo). So the codegen + DB step is namedsetupand run explicitly, not on install.
There is no point0 start command — production runs the built server entry
directly; see deploy. point0 dev and point0 build are thin
wrappers over engine.dev() / engine.build() — full flag lists in cli,
dev, and build. --hot hot-swaps edited points without
restarting the server; it's marked experimental in the CLI help.
Every process in the dev tree dies with whatever launched it: the point0 CLI
spawns each dev child with Bun's --no-orphans flag (inherited by nested bun
processes), and the scaffolded bunfig.toml sets [run] noOrphans = true so
the bun run dev wrapper itself dies with the terminal. Close the terminal and
the dev server (and all its children) exits with it — no orphaned servers
holding ports.
--no-orphans landed in Bun 1.3.14, so that's the floor — the generated
package.json declares it as "engines": { "bun": ">=1.3.14" }. On an older
Bun the dev server still runs, but it can leak orphaned processes that keep
holding ports; upgrade with bun upgrade.
Everything below is what the scaffolder writes for you — skip this section if
you used bun create point0-app. Point0 is just a set of packages, so you can
assemble an app from a handful of files. Each one is cited from the template so
you can copy it.
The four core scripts map directly to the point0 CLI:
{
"scripts": {
"dev": "point0 dev --hot", // dev server + clients, server hot reload
"generate": "point0 generate", // write src/generated/point0/* from your points
"build": "point0 build", // build server + clients into dist/
"start": "cross-env NODE_ENV=production bun run ./dist/server/index.server.js",
},
}The supporting scripts complete the setup chain — Prisma helpers, a seed, and typecheck:
{
"scripts": {
"setup": "bun run prisma:migrate:deploy && bun run prisma:generate && point0 generate && bun run seed",
"seed": "cross-env NODE_ENV=development bun --preload ./src/preload.ts ./src/lib/seed.ts",
"prisma:generate": "cross-env NODE_ENV=development prisma generate",
"prisma:migrate:deploy": "cross-env NODE_ENV=development prisma migrate deploy",
"types": "tsc --noEmit",
},
}seed runs with --preload ./src/preload.ts — any direct bun src/<file> that
imports app code needs the preload first, so the point0 compiler plugins are
registered before that code loads (more on this below).
src/engine.ts — the engine configEverything starts from one Engine.create(...) call. It declares SSR, where to
find points, where to write generated code, and one config block per side
(server, client):
// src/engine.ts (Bun-bundled form)
import { Engine } from '@point0/engine'
import { clientEnvKeys } from '@/lib/env/client-shape'
export const engine = Engine.create({
file: import.meta.url, // engine anchors all relative paths to this file
ssr: true,
pointsGlob: '**/*.{ts,tsx,mdx}', // where points live
generate: {
meta: './generated/point0/meta.ts',
assetsTypes: './generated/point0/assets.d.ts',
},
server: {
scope: 'root',
port: process.env.SERVER_PORT || process.env.PORT,
entry: { main: './index.server.ts' },
points: async () => await import('./generated/point0/points.server'),
generate: { points: './generated/point0/points.server.ts' },
outdir: '../dist/server',
},
client: {
scope: 'root',
port: process.env.CLIENT_PORT,
indexHtml: './index.html',
app: async () => await import('./app.client'),
points: async () => await import('./generated/point0/points.client'),
generate: {
points: './generated/point0/points.client.ts',
routes: {
outfile: './generated/point0/routes.ts',
origin: 'process.env.CLIENT_URL',
},
},
bunPlugins: ['bun-plugin-tailwind'], // Vite mode replaces this with `viteConfig`
env: { vars: clientEnvKeys }, // which env keys are bundled into the client
publicdir: {
source: '../public',
outdir: '../dist/client',
},
outdir: '../dist/client',
},
})Every option here has a page: the full schema is engine-config,
the codegen keys are generator, publicdir is
publicdir, and env.vars is env. The examples/basic
engine adds one extra block —
client.compiler.babel: ['babel-plugin-react-compiler'] — which the template
omits.
Around the engine sit four small files. Order is load-bearing in two of them.
src/preload.ts — registers the point0 compiler plugins and env constants.
Anything that imports app code must run this first:
// src/preload.ts
import { engine } from '@/engine'
await engine.preload({ nodeEnvFallback: 'development' })src/index.server.ts — the server entry (this is what start runs in
production, as dist/server/index.server.js). Preload before app code, via
dynamic imports so the order holds:
// src/index.server.ts
await import('./preload.js')
await import('./app.server.js') // imports @/engine and calls engine.serve()
export {}app.server.ts validates the server env, then calls await engine.serve(). See
engine-runtime for serve() and the rest of the engine
instance.
src/app.client.tsx — the React app shell, default-exported. It nests the
providers your points expect (react-query, unhead) and mounts the router:
// src/app.client.tsx
import { Router, RouterRoutes } from '@/lib/navigation'
import { UnheadProvider } from '@point0/core/unhead'
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from '@/lib/query-client'
import { Head } from '@unhead/react'
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<UnheadProvider>
<Head>
{/* global head tags; per-page ones come from `.head(...)` on each point */}
<link rel="shortcut icon" href="/favicon.ico" />
</Head>
<Router>
<RouterRoutes />
</Router>
</UnheadProvider>
</QueryClientProvider>
)
}src/index.client.tsx — the browser entry. It hands your App and the
generated points to mount from @point0/react-dom/mount:
// src/index.client.tsx
import '@/lib/env/client' // validate client env before mounting
import App from '@/app.client'
import points from '@/generated/point0/points.client'
import '@/styles/index.css'
import { ErrorBoundary } from '@/ui/error-boundary'
import { mount } from '@point0/react-dom/mount'
mount(
<ErrorBoundary>
<App />
</ErrorBoundary>,
points,
)
if (import.meta.hot) {
import.meta.hot.accept()
}mount mounts into #root by default (it throws if #root is missing) and
hydrates from the SSR-dehydrated store when one is present. index.html is the
HTML template the client builds from — a #root div plus a
<script type="module" src="./index.client.tsx">.
The four entry files above are the wiring; the rest of the template is the working example built on top of them:
src/lib/root.tsx — the root point (Point0.lets.root()...),
where you set the transformer, schema helper, error class, prefetch policy,
and global middleware like openapi. Every page and query grows from
here.src/lib/navigation.ts — the router wiring (createNavigation over
wouter), exporting Link, navigate, Router, RouterRoutes, etc. See
navigation.src/lib/query-client.ts, src/lib/prisma.ts, src/lib/error.ts
(a custom AppError class wired in via .errorClass(...) — the template
builds it with Error0, but any compatible class works, and
the default is ErrorPoint0), and the five-file src/lib/env/* system
(shared / client / server env shapes and validators).src/layouts/general.tsx (a layout),
src/pages/home.tsx (a page), src/pages/about.mdx (an mdx
page). The home page doubles as a playground: right in home.tsx it declares
ideaListQuery (a query) over the Idea Prisma model and
ideaCreateMutation (a mutation), plus a form that creates rows
and invalidates the list. src/lib/seed.ts seeds three ideas (setup runs
it).src/generated/ — never hand-edited; produced by point0 generate and
prisma generate, and gitignored. See generator.Plus project-level files: package.json, tsconfig.json (with the @/* →
./src/* path alias), bunfig.toml, prisma.config.ts +
prisma/schema.prisma, env.example, a Dockerfile, and .gitignore.
Gotcha — the template
docker-compose.ymlpoints at the monorepo. It referencesexamples/basic(build context../..), a stale artifact that does not work in a standalone generated app. Don't rely ondocker composefrom the scaffolded app until you rewrite it — see deploy for the real deploy path.
| Script | Command | CLI page |
|---|---|---|
dev | point0 dev --hot | dev |
build | point0 build | build |
generate | point0 generate | generator |
start | bun run ./dist/server/index.server.js (prod) | deploy |
setup | prisma migrate + generate + point0 generate + seed | — |
| File | Role |
|---|---|
src/engine.ts | Engine.create(...) — the whole app config |
src/preload.ts | registers compiler plugins + env consts (engine.preload) |
src/index.server.ts | server entry; preload, then app.server (engine.serve) |
src/app.client.tsx | React app shell (providers + router), default export |
src/index.client.tsx | browser entry; mount(<App/>, points) |