newX 1.3 — islands to disk, server-mode islands, image proxy

Migration Guide

Migrate an existing app to X

Move a Next.js or TanStack app to x piece by piece. x shares the file-based, React-islands model you already know, so most of your components, loaders, and API routes port over with minimal churn.

Conceptual mapping

x is built around the same ideas you already use, under slightly different names:

Next.js / RemixxNotes
app/ or pages/ dirsrc/pagesFile-based routes; _layout.tsx layers nest by directory.
getServerSideProps / loader()loader()Same contract: async, receives params/request, returns data.
export const revalidate / ISRexport const revalidateIdentical semantics: static page + N-second cache.
Server ActionsServer functionsAsync exports under src/actions become POST endpoints.
route.ts / api/ handlerssrc/api/*.tsNamed GET/POST/PUT/PATCH/DELETE exports.
middleware.tsMiddleware_middleware.ts or export const middleware; onion pattern.
Client components with hydration boundariesIslandsexport const islands in a page; <Island> hydrates them.

1. Scaffold the shell

Create a fresh X project and copy your package files, styles, and public assets into it:

terminal
bun create thexjs-app@latest my-app
cd my-app
cp -r old-project/public .
cp old-project/x.config.ts . 2>/dev/null || true

2. Port your data layer

Move schemas into versioned SQL migration files and run them at boot. x ships migration runners for both SQLite and Postgres that track applied files in a _x_migrations table:

data/migrations/001_create_users.sql
CREATE TABLE users (  id TEXT PRIMARY KEY,  email TEXT NOT NULL UNIQUE,  name TEXT);
src/lib/db.ts
import { connectSQLite, runSQLiteMigrations } from "@thexjs/core/data";import { join } from "node:path";const db = connectSQLite(process.env.SQLITE_PATH ?? "./data/app.db");runSQLiteMigrations(db, join(import.meta.dir, "..", "data", "migrations"));export default db;

3. Port pages + loaders

A server-rendered page with a loader is almost identical to the Next.js version. Change the export name and drop the framework-specific glue:

src/pages/posts/[slug].tsx
import type { LoaderArgs, RouteProps } from "@thexjs/core";import db from "../lib/db";export async function loader({ params }: LoaderArgs) {  const post = db.query("SELECT * FROM posts WHERE slug = ?").get(params.slug);  if (!post) return { status: 404 };  return post;}export default function Post({ loaderData }: RouteProps) {  const post = loaderData as { title: string; body: string };  return (    <article>      <h1>{post.title}</h1>      <p>{post.body}</p>    </article>  );}

Static pages set export const mode = "static" and export const revalidate for time-based regeneration, just like ISR:

src/pages/blog.tsx
export const mode = "static";export const revalidate = 3600;

4. Server functions

Fold per-route server actions into a shared module under src/actions. Call them from loaders, API routes, forms, or islands -- the client bundle only ships a fetch() wrapper, never the server implementation:

src/actions/newsletter.ts
export async function subscribe(email: string) {  const user = await db.query("SELECT id FROM users WHERE email = ?").get(email);  if (!user) throw new Error("No account found for that email");  await db.run("INSERT INTO subscribers (email) VALUES (?)", [email]);  return { ok: true };}
src/pages/newsletter.tsx
import { subscribe } from "../actions/newsletter";export const islands = { Form };function Form() {  return (    <form onSubmit={(e) => {      e.preventDefault();      subscribe(new FormData(e.currentTarget).get("email"));    }}>      <input name="email" type="email" /> >      <button>Subscribe</button>    </form>  );}

5. API routes

Replace route.ts handlers with named export functions in src/api. Each receives the Request and returns a Response:

src/api/health.ts
export function GET(request: Request) {  return Response.json({ ok: true, ts: Date.now() });}export async function POST(request: Request) {  const body = await request.json();  // ...  return Response.json({ received: true }, { status: 201 });}

6. Build, doctor, deploy

Run the diagnostics command to catch env-isolation violations, missing dirs, or dependency mismatches before you build, then ship like you did before:

terminal
x doctor # config, dirs, env isolation, deps
x build # -> .x/client + .x/server (Bun server)
x build --adapter vercel # -> .vercel/output Build Output API v3

What doesn't change

  • React stays React -- x renders with react-dom server-side and hydrates islands client-side.
  • Plain CSS, Tailwind (compiled by the dev server), and content collections slot in as-is.
  • Your environment split: variables prefixed THEXJS_PUBLIC_ reach the client; everything else stays server-only.
  • Deploy targets: a self-hosted Bun server, or Vercel via the bundled adapter.

Next step: read the Getting Started guide for the full walkthrough, or jump into Server Functions. If something from your framework doesn't map cleanly, open an issue -- the migration story is being actively tuned.