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 refs✓——partial
Typed mutationscaller knows the input type✓FormData onlyFormData only✓
Schema-validated actionsZod / Valibot / ArkType, typed field errors✓manualmanual✓
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 action✓origin-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 bundles✓partial—partial
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.