place

Why place

Three frameworks already own this space. Each made one structural mistake we're not making — and the difference shows up in code you write every day.

Honest framing
Next, Remix, and TanStack Start are all good. place isn't a strict upgrade — it's a different bet about what the framework should hide and what it should expose.

Hello, world

The smallest unit — declare a route. Same outcome, three philosophies.

ts
// src/pages/hello.page.tsx import { page } from '@place-ts/component' export default page('/hello', { view: () => <h1>Hello</h1>, })

Next and Remix both encode the route in the file path. Move the file, your route moves with it; references to it (links, action callers) get stale. place puts the route in a value — refactor it, TypeScript flags every call site.

Server actions

Mutation is the API people get wrong first. The 'use server' marker hides too much; the FormData contract throws away types.

ts
export default page('/posts/:id', { on: { save: async (input: { title: string }, { params }) => { await db.posts.update(params.id, input) return { ok: true } }, }, view: () => /* call pageRef.save({...}) — fully typed */ null, })

place's action lives on the same page as its caller, with the full input type intact. The endpoint is visible (POST /posts/:id/_action/save); the path appears in your routes table; no Babel pass, no encrypted action IDs, no untyped FormData detour.

Capabilities, not context

React's context is global by default and silent on SSR mismatches. Capabilities are typed, scoped, and SSR-aware out of the box.

ts
import { defineCapability } from '@place-ts/capability' export const NoteStoreCap = defineCapability<NoteStore>('NoteStore', { clientOnly: true, }) // In a page: const store = NoteStoreCap.use() // typed; provided once at app config

clientOnly: true auto-emits an SSR-safe placeholder when a browser-only cap is touched during render. No typeof window branches. No hydration mismatches.

Feature matrix

placeNext.js (App Router)RemixTanStack Start
Routeshow routes are declaredvaluesfile conventionfile conventionfile convention
Refactor by renameTS catches stale route refspartial
Typed mutationscaller knows the input typeFormData onlyFormData only
Schema-validated actionsZod / Valibot / ArkType, typed field errorsmanualmanual
No use client / use server markersno magic string directives
No codegen stepno generated files to regenerate
SSR-safe typed contextcapabilities — no typeof window checks
Islands hydrationcontent pages ship zero framework JS
Streaming SSRper-suspense boundaries
Built-in CSRFautomatic on every actionorigin-check
Built-in CSP + SRIstrict headers, hashed scripts
Image componentsrcset + lazy variants + cachingpluggable
Component library included@place-ts/design — 14 primitives
Animation primitivesspring / tween / sequence as derived state
RBAC gate component<Can do="…"> reads the session
View Transitionsopt-in, zero extra JS
Islands-aware static exportpre-render + ship only island bundlespartialpartial
Bundle — hello worldgzipped client JS, content page0 KB74 KB38 KB52 KB
place ships an islands hydration model: a page with no interactive island ships zero framework JavaScript — a "hello world" content page is literally 0 KB on the client. Pages that need interactivity ship only their islands' bundles (typically 3–8 KB each), never a whole-app runtime. Competitor numbers are a minimal "hello world" with the default router; the relative gap widens, not narrows, as apps grow.

When not to pick place

  • You need Node-specific integrations. place runs on Bun; the first-party adapters ship for Cloudflare Workers, Vercel Build Output, and Deno Deploy via createFetchHandler(), but pure-Node compatibility is best-effort.
  • You have a large React codebase with deep React-specific patterns. place uses a different reactivity model; the migration cost is real.
  • You need an established ecosystem. place is v0.x; the recipe library will be 10× smaller than Next's for a while.