@pylonsync/create-pylon 0.3.229 → 0.3.231

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -71,6 +71,16 @@ const PYLON_VERSION = JSON.parse(
71
71
  const PLATFORMS_AVAILABLE = ["web", "vite", "ios", "mac", "expo"];
72
72
 
73
73
  const TEMPLATE_REGISTRY = {
74
+ ssr: {
75
+ blurb:
76
+ "Full-stack SSR — server-rendered React + Link/Image/Tailwind, one server, no Next.js.",
77
+ // `unified` templates are a single Pylon app (app.ts + app/ routes +
78
+ // functions/), NOT a monorepo of apps/api + apps/web. `pylon dev`
79
+ // serves the SSR frontend and the API from one port. They take no
80
+ // `--platforms` — the app IS the whole stack.
81
+ platforms: [],
82
+ unified: true,
83
+ },
74
84
  barebones: {
75
85
  blurb: "Single entity, list + create. The smallest working app.",
76
86
  platforms: ["web", "ios", "mac", "expo"],
@@ -128,7 +138,8 @@ function pickValue(arr, ...candidates) {
128
138
 
129
139
  if (flags.help) {
130
140
  const tmplLines = Object.entries(TEMPLATE_REGISTRY).map(
131
- ([k, v]) => ` ${k.padEnd(10)} ${v.blurb} (${v.platforms.join(", ")})`,
141
+ ([k, v]) =>
142
+ ` ${k.padEnd(10)} ${v.blurb} (${v.unified ? "single app" : v.platforms.join(", ")})`,
132
143
  );
133
144
  process.stdout.write(`
134
145
  Usage: npm create @pylonsync/pylon [name] [options]
@@ -137,10 +148,12 @@ Usage: npm create @pylonsync/pylon [name] [options]
137
148
  ${tmplLines.join("\n")}
138
149
 
139
150
  --platforms <list> comma list: ${PLATFORMS_AVAILABLE.join(",")} (default: web)
151
+ ignored for ssr — it's a single full-stack app, no platforms
140
152
  --bun|--pnpm|--yarn|--npm
141
153
  --skip-install scaffold only, don't run install
142
154
 
143
155
  Examples:
156
+ npm create @pylonsync/pylon my-app --template ssr # full-stack SSR, no Next.js
144
157
  npm create @pylonsync/pylon my-app
145
158
  npm create @pylonsync/pylon my-app --template todo --platforms web,ios
146
159
  npm create @pylonsync/pylon my-app --template b2b --platforms web,mac
@@ -167,7 +180,10 @@ if (!flags.template) {
167
180
  .toLowerCase();
168
181
  flags.template = TEMPLATES_AVAILABLE.includes(ans) ? ans : "todo";
169
182
  }
170
- if (!flags.platforms) {
183
+ // `unified` templates (ssr) are a single app, not a monorepo — they take
184
+ // no platforms. Skip the platform prompt + validation for them entirely.
185
+ const isUnified = TEMPLATE_REGISTRY[flags.template]?.unified === true;
186
+ if (!isUnified && !flags.platforms) {
171
187
  const supported = TEMPLATE_REGISTRY[flags.template].platforms.join(", ");
172
188
  const ans = (
173
189
  await rl.question(
@@ -188,29 +204,35 @@ if (!flags.pm) {
188
204
  }
189
205
  rl.close();
190
206
 
191
- const platforms = flags.platforms
192
- .split(",")
193
- .map((p) => p.trim().toLowerCase())
194
- .filter(Boolean);
195
- const unknownPlatforms = platforms.filter(
196
- (p) => !PLATFORMS_AVAILABLE.includes(p),
197
- );
198
- if (unknownPlatforms.length > 0) {
199
- console.error(
200
- `\nError: unknown platform(s): ${unknownPlatforms.join(", ")}. Valid: ${PLATFORMS_AVAILABLE.join(", ")}\n`,
201
- );
202
- exit(1);
203
- }
204
- if (platforms.length === 0) {
205
- console.error(`\nError: at least one platform required.\n`);
206
- exit(1);
207
- }
208
- if (platforms.includes("web") && platforms.includes("vite")) {
209
- console.error(
210
- `\nError: --platforms web and vite are mutually exclusive (both render into apps/web).\n` +
211
- ` Pick one: web for Next.js 16, vite for plain Vite + React.\n`,
207
+ // Unified templates own no platforms they ARE the whole app. For
208
+ // everything else, parse + validate the platform list.
209
+ const platforms = isUnified
210
+ ? []
211
+ : (flags.platforms ?? "web")
212
+ .split(",")
213
+ .map((p) => p.trim().toLowerCase())
214
+ .filter(Boolean);
215
+ if (!isUnified) {
216
+ const unknownPlatforms = platforms.filter(
217
+ (p) => !PLATFORMS_AVAILABLE.includes(p),
212
218
  );
213
- exit(1);
219
+ if (unknownPlatforms.length > 0) {
220
+ console.error(
221
+ `\nError: unknown platform(s): ${unknownPlatforms.join(", ")}. Valid: ${PLATFORMS_AVAILABLE.join(", ")}\n`,
222
+ );
223
+ exit(1);
224
+ }
225
+ if (platforms.length === 0) {
226
+ console.error(`\nError: at least one platform required.\n`);
227
+ exit(1);
228
+ }
229
+ if (platforms.includes("web") && platforms.includes("vite")) {
230
+ console.error(
231
+ `\nError: --platforms web and vite are mutually exclusive (both render into apps/web).\n` +
232
+ ` Pick one: web for Next.js 16, vite for plain Vite + React.\n`,
233
+ );
234
+ exit(1);
235
+ }
214
236
  }
215
237
  if (!TEMPLATES_AVAILABLE.includes(flags.template)) {
216
238
  console.error(
@@ -251,7 +273,7 @@ if (existsSync(root) && readdirSync(root).length > 0) {
251
273
  mkdirSync(root, { recursive: true });
252
274
 
253
275
  console.log(
254
- `\nCreating ${projectName} (${flags.template}, ${platforms.join(" + ")}) in ${root}\n`,
276
+ `\nCreating ${projectName} (${flags.template}${isUnified ? "" : `, ${platforms.join(" + ")}`}) in ${root}\n`,
255
277
  );
256
278
 
257
279
  // ---------------------------------------------------------------------------
@@ -274,6 +296,7 @@ const SUBS = {
274
296
  __APP_NAME_PASCAL__: APP_NAME_PASCAL,
275
297
  __PYLON_VERSION__: PYLON_VERSION,
276
298
  __WORKSPACE_DEP__: workspaceDepSpec,
299
+ __RUN_DEV__: flags.pm === "npm" ? "npm run dev" : `${flags.pm} run dev`,
277
300
  };
278
301
 
279
302
  // Filenames that contain placeholders get renamed AFTER copy. Keeps
@@ -302,7 +325,12 @@ function substituteFile(absPath) {
302
325
  function walkAndSubstitute(dir) {
303
326
  for (const entry of readdirSync(dir)) {
304
327
  const abs = join(dir, entry);
305
- const renamed = substituteString(entry);
328
+ let renamed = substituteString(entry);
329
+ // npm strips a literal `.gitignore` from published tarballs, so
330
+ // templates ship it as `gitignore` and we restore the dot at
331
+ // scaffold time — otherwise the new project has no ignore file
332
+ // and node_modules / .pylon / *.db get committed.
333
+ if (renamed === "gitignore") renamed = ".gitignore";
306
334
  let target = abs;
307
335
  if (renamed !== entry) {
308
336
  target = join(dir, renamed);
@@ -332,85 +360,94 @@ function copyTemplate(srcSubpath, destSubpath = "") {
332
360
  // 5. Root package.json — generated, not templated
333
361
  // ---------------------------------------------------------------------------
334
362
 
335
- copyTemplate("_root");
336
- copyTemplate(`backend/${flags.template}`);
337
-
338
- // `web` (Next.js) and `vite` are alternative web-frontend toolchains;
339
- // the mutex check above guarantees at most one of them is set. Either
340
- // way we also pull in packages/ui so the shared primitives are present.
341
- if (platforms.includes("web")) {
342
- copyTemplate("ui");
343
- copyTemplate(`web/${flags.template}`);
344
- }
345
- if (platforms.includes("vite")) {
346
- copyTemplate("ui");
347
- copyTemplate(`vite/${flags.template}`);
348
- }
349
- for (const p of ["ios", "mac", "expo"]) {
350
- if (platforms.includes(p)) copyTemplate(`${p}/${flags.template}`);
363
+ if (isUnified) {
364
+ // Single unified app: app.ts + app/ routes + functions/, served by
365
+ // `pylon dev` (frontend + API, one port). The template ships its own
366
+ // package.json no monorepo root, no turbo, no workspaces.
367
+ copyTemplate(flags.template);
368
+ } else {
369
+ copyTemplate("_root");
370
+ copyTemplate(`backend/${flags.template}`);
371
+
372
+ // `web` (Next.js) and `vite` are alternative web-frontend toolchains;
373
+ // the mutex check above guarantees at most one of them is set. Either
374
+ // way we also pull in packages/ui so the shared primitives are present.
375
+ if (platforms.includes("web")) {
376
+ copyTemplate("ui");
377
+ copyTemplate(`web/${flags.template}`);
378
+ }
379
+ if (platforms.includes("vite")) {
380
+ copyTemplate("ui");
381
+ copyTemplate(`vite/${flags.template}`);
382
+ }
383
+ for (const p of ["ios", "mac", "expo"]) {
384
+ if (platforms.includes(p)) copyTemplate(`${p}/${flags.template}`);
385
+ }
351
386
  }
352
387
 
353
388
  walkAndSubstitute(root);
354
389
 
355
390
  // ---------------------------------------------------------------------------
356
- // Root package.json — generated based on selected platforms. Workspace
357
- // scripts depend on which apps exist + which package manager the user
358
- // picked (each PM exposes "run X in workspace Y" differently).
391
+ // Root package.json — generated based on selected platforms (monorepo
392
+ // templates only). Unified templates ship their own single-app
393
+ // package.json, so skip this entirely.
359
394
  // ---------------------------------------------------------------------------
360
395
 
361
- // Turborepo orchestrates the workspace. `turbo dev` runs the `dev`
362
- // task in every package that defines one (apps/api always; apps/web,
363
- // apps/expo when scaffolded). Native targets (ios, mac) aren't
364
- // `turbo dev`-shaped — Xcode / `swift run` block so they get
365
- // dedicated escape-hatch scripts instead. turbo.json ships in
366
- // _root/, so it's already in the project.
367
- const helperScripts = {};
368
- if (platforms.includes("ios")) {
369
- helperScripts["dev:ios"] =
370
- "echo 'cd apps/ios && xcodegen generate && open *.xcodeproj (or: swift run for a quick macOS preview)'";
371
- }
372
- if (platforms.includes("mac")) {
373
- helperScripts["dev:mac"] =
374
- "echo 'cd apps/mac && swift run (or: xcodegen generate && open *.xcodeproj)'";
375
- }
376
-
377
- // Turbo 2.x refuses to run without packageManager set. Pick a recent-
378
- // stable for whichever PM the user picked. npm doesn't enforce this
379
- // field but turbo still expects it to be present.
380
- const PACKAGE_MANAGERS = {
381
- bun: "bun@1.2.19",
382
- pnpm: "pnpm@9.12.0",
383
- yarn: "yarn@4.5.0",
384
- npm: "npm@10.9.0",
385
- };
396
+ if (!isUnified) {
397
+ // Turborepo orchestrates the workspace. `turbo dev` runs the `dev`
398
+ // task in every package that defines one (apps/api always; apps/web,
399
+ // apps/expo when scaffolded). Native targets (ios, mac) aren't
400
+ // `turbo dev`-shaped Xcode / `swift run` block — so they get
401
+ // dedicated escape-hatch scripts instead. turbo.json ships in
402
+ // _root/, so it's already in the project.
403
+ const helperScripts = {};
404
+ if (platforms.includes("ios")) {
405
+ helperScripts["dev:ios"] =
406
+ "echo 'cd apps/ios && xcodegen generate && open *.xcodeproj (or: swift run for a quick macOS preview)'";
407
+ }
408
+ if (platforms.includes("mac")) {
409
+ helperScripts["dev:mac"] =
410
+ "echo 'cd apps/mac && swift run (or: xcodegen generate && open *.xcodeproj)'";
411
+ }
386
412
 
387
- const rootPkg = {
388
- name: APP_NAME_KEBAB,
389
- private: true,
390
- type: "module",
391
- packageManager: PACKAGE_MANAGERS[flags.pm],
392
- workspaces: ["apps/*", "packages/*"].filter((p) => {
393
- // Only declare packages/* as a workspace if we actually scaffolded
394
- // packages/ui — otherwise the empty match warns on bun install.
395
- if (p === "packages/*")
396
- return platforms.includes("web") || platforms.includes("vite");
397
- return true;
398
- }),
399
- scripts: {
400
- dev: "turbo dev",
401
- build: "turbo build",
402
- check: "turbo check",
403
- lint: "turbo lint",
404
- ...helperScripts,
405
- },
406
- devDependencies: {
407
- turbo: "^2.3.0",
408
- },
409
- };
410
- writeFileSync(
411
- join(root, "package.json"),
412
- JSON.stringify(rootPkg, null, 2) + "\n",
413
- );
413
+ // Turbo 2.x refuses to run without packageManager set. Pick a recent-
414
+ // stable for whichever PM the user picked. npm doesn't enforce this
415
+ // field but turbo still expects it to be present.
416
+ const PACKAGE_MANAGERS = {
417
+ bun: "bun@1.2.19",
418
+ pnpm: "pnpm@9.12.0",
419
+ yarn: "yarn@4.5.0",
420
+ npm: "npm@10.9.0",
421
+ };
422
+
423
+ const rootPkg = {
424
+ name: APP_NAME_KEBAB,
425
+ private: true,
426
+ type: "module",
427
+ packageManager: PACKAGE_MANAGERS[flags.pm],
428
+ workspaces: ["apps/*", "packages/*"].filter((p) => {
429
+ // Only declare packages/* as a workspace if we actually scaffolded
430
+ // packages/ui — otherwise the empty match warns on bun install.
431
+ if (p === "packages/*")
432
+ return platforms.includes("web") || platforms.includes("vite");
433
+ return true;
434
+ }),
435
+ scripts: {
436
+ dev: "turbo dev",
437
+ build: "turbo build",
438
+ check: "turbo check",
439
+ lint: "turbo lint",
440
+ ...helperScripts,
441
+ },
442
+ devDependencies: {
443
+ turbo: "^2.3.0",
444
+ },
445
+ };
446
+ writeFileSync(
447
+ join(root, "package.json"),
448
+ JSON.stringify(rootPkg, null, 2) + "\n",
449
+ );
450
+ }
414
451
 
415
452
  // ---------------------------------------------------------------------------
416
453
  // Optional: install dependencies
@@ -437,7 +474,10 @@ if (!flags.skipInstall) {
437
474
  const runDev = flags.pm === "npm" ? "npm run dev" : `${flags.pm} run dev`;
438
475
 
439
476
  const platformLines = [];
440
- platformLines.push(" → api http://localhost:4321 (Pylon control plane)");
477
+ if (isUnified)
478
+ platformLines.push(" → app http://localhost:4321 (SSR frontend + API, one port)");
479
+ else
480
+ platformLines.push(" → api http://localhost:4321 (Pylon control plane)");
441
481
  if (platforms.includes("web"))
442
482
  platformLines.push(" → web http://localhost:3000 (Next.js)");
443
483
  if (platforms.includes("vite"))
@@ -449,8 +489,15 @@ if (platforms.includes("ios"))
449
489
  if (platforms.includes("mac"))
450
490
  platformLines.push(` → mac cd apps/mac && swift run (or xcodegen for .app)`);
451
491
 
452
- const layoutLines = [" apps/api schema + functions/ handlers"];
453
- if (platforms.includes("web")) {
492
+ const layoutLines = isUnified
493
+ ? [
494
+ " app.ts data model + manifest (entities, functions, routes)",
495
+ " app/ file-based SSR routes (app/page.tsx → /)",
496
+ " app/layout.tsx root layout (receives url + auth)",
497
+ " functions/ server functions (query/action)",
498
+ ]
499
+ : [" apps/api schema + functions/ handlers"];
500
+ if (!isUnified && platforms.includes("web")) {
454
501
  layoutLines.push(" apps/web Next.js 16 + React 19 + Tailwind v4");
455
502
  layoutLines.push(" packages/ui shared UI primitives");
456
503
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/create-pylon",
3
- "version": "0.3.229",
3
+ "version": "0.3.231",
4
4
  "description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,30 @@
1
+ node_modules/
2
+ .next/
3
+ .turbo/
4
+ dist/
5
+ out/
6
+ .env
7
+ .env.local
8
+ *.db
9
+ *.db-journal
10
+ # Local pylon dev state: sqlite db, jobs queue, sessions, uploads dir.
11
+ # Created by `pylon dev` on first run; safe to delete to reset.
12
+ .pylon/
13
+ apps/api/pylon.manifest.json
14
+ apps/api/pylon.client.ts
15
+
16
+ # Swift / Xcode
17
+ .build/
18
+ DerivedData/
19
+ *.xcworkspace
20
+ *.xcodeproj
21
+ Package.resolved
22
+
23
+ # Expo / RN
24
+ .expo/
25
+ .expo-shared/
26
+ ios/Pods/
27
+ ios/build/
28
+ android/.gradle/
29
+ android/build/
30
+ android/app/build/
@@ -0,0 +1,42 @@
1
+ # __APP_NAME__
2
+
3
+ A full-stack [Pylon](https://pylonsync.com) app — server-rendered React,
4
+ file-based routes, a synced database, and a typed client, served from one
5
+ binary on one port. No Next.js, no separate API server.
6
+
7
+ ## Develop
8
+
9
+ ```bash
10
+ __RUN_DEV__
11
+ ```
12
+
13
+ Open http://localhost:4321. Edit any file under `app/` and save — the page
14
+ reloads instantly.
15
+
16
+ ## Layout
17
+
18
+ ```
19
+ app.ts your data model + manifest (entities, functions, policies, routes)
20
+ app/ file-based SSR routes — app/page.tsx is "/", app/counter/page.tsx is "/counter"
21
+ app/layout.tsx the root layout wrapping every page (receives url + auth)
22
+ app/globals.css Tailwind entrypoint (compiled by Pylon)
23
+ functions/ server functions (query/action) — typed RPC, auto-exposed
24
+ ```
25
+
26
+ ## Add a route
27
+
28
+ Drop a file at `app/about/page.tsx` and visit `/about`. Pages receive
29
+ `{ url, auth, searchParams }` from the SSR runtime.
30
+
31
+ ## Add data
32
+
33
+ Edit `app.ts`. Every `entity()` becomes a synced table with a REST +
34
+ realtime API and a typed client — no migrations, no resolvers.
35
+
36
+ ## Deploy
37
+
38
+ ```bash
39
+ pylon deploy
40
+ ```
41
+
42
+ Docs: https://docs.pylonsync.com
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+
3
+ interface PageProps {
4
+ url: string;
5
+ searchParams: Record<string, string>;
6
+ }
7
+
8
+ // `app/counter/page.tsx` → `/counter`. This page is server-rendered AND
9
+ // interactive: the HTML arrives with the initial count already in it (try
10
+ // /counter?start=10), then the per-route chunk hydrates and useState takes
11
+ // over. No client/server split to manage — it's one component.
12
+ export default function CounterPage({ searchParams }: PageProps) {
13
+ const start = Number(searchParams.start ?? "0") || 0;
14
+ const [count, setCount] = React.useState(start);
15
+ return (
16
+ <div className="space-y-6">
17
+ <h1 className="text-2xl font-semibold tracking-tight">Counter</h1>
18
+ <p className="text-zinc-600">
19
+ Rendered on the server, hydrated in the browser. The buttons work
20
+ because the page's JS chunk hydrated this exact markup.
21
+ </p>
22
+ <div className="flex items-center gap-4">
23
+ <button
24
+ onClick={() => setCount((c) => c - 1)}
25
+ className="rounded-lg border border-zinc-300 px-4 py-2 text-lg hover:bg-zinc-100"
26
+ >
27
+
28
+ </button>
29
+ <span className="min-w-12 text-center text-2xl font-semibold tabular-nums">
30
+ {count}
31
+ </span>
32
+ <button
33
+ onClick={() => setCount((c) => c + 1)}
34
+ className="rounded-lg border border-zinc-300 px-4 py-2 text-lg hover:bg-zinc-100"
35
+ >
36
+ +
37
+ </button>
38
+ </div>
39
+ <p className="text-xs text-zinc-500">
40
+ Initial value comes from <code>?start=</code> — search params flow
41
+ through SSR. Try{" "}
42
+ <a
43
+ href="/counter?start=10"
44
+ className="text-blue-600 underline-offset-4 hover:underline"
45
+ >
46
+ /counter?start=10
47
+ </a>
48
+ .
49
+ </p>
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,3 @@
1
+ @import "tailwindcss";
2
+
3
+ @source "../app/**/*.{tsx,ts,jsx,js}";
@@ -0,0 +1,66 @@
1
+ import React from "react";
2
+ import { Link } from "@pylonsync/react";
3
+
4
+ // Auth shape injected by the SSR runtime. `auth.user_id` is null for
5
+ // anonymous visitors. Wire a sign-in flow with @pylonsync/client when
6
+ // you're ready — for now this just shows the session state.
7
+ interface AuthShape {
8
+ user_id: string | null;
9
+ is_admin: boolean;
10
+ tenant_id: string | null;
11
+ roles: string[];
12
+ }
13
+
14
+ interface LayoutProps {
15
+ children: React.ReactNode;
16
+ url: string;
17
+ auth: AuthShape;
18
+ }
19
+
20
+ // The root layout wraps every page. It receives `url` and `auth` from the
21
+ // SSR runtime on every render — server-side, before the HTML is sent.
22
+ export default function RootLayout({ children, url, auth }: LayoutProps) {
23
+ const signedIn = Boolean(auth?.user_id);
24
+ return (
25
+ <html lang="en" className="bg-zinc-50">
26
+ <head>
27
+ <meta charSet="utf-8" />
28
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
29
+ <title>__APP_NAME__</title>
30
+ {/* Tailwind is compiled by Pylon from app/globals.css and the
31
+ stylesheet link is injected here automatically — nothing to
32
+ wire up. */}
33
+ </head>
34
+ <body className="min-h-screen text-zinc-900 antialiased">
35
+ <header className="sticky top-0 z-10 border-b border-zinc-200 bg-white/80 backdrop-blur">
36
+ <div className="mx-auto flex max-w-3xl items-center justify-between px-4 py-3">
37
+ <Link
38
+ href="/"
39
+ className="text-sm font-semibold tracking-tight hover:text-zinc-600"
40
+ >
41
+ __APP_NAME__
42
+ </Link>
43
+ <nav className="flex items-center gap-4 text-sm text-zinc-600">
44
+ <Link href="/" className="hover:text-zinc-900">
45
+ Home
46
+ </Link>
47
+ <Link href="/counter" className="hover:text-zinc-900">
48
+ Counter
49
+ </Link>
50
+ <span
51
+ className={signedIn ? "text-emerald-600" : "text-zinc-400"}
52
+ title={url}
53
+ >
54
+ {signedIn ? `· ${auth.user_id}` : "· anon"}
55
+ </span>
56
+ </nav>
57
+ </div>
58
+ </header>
59
+ <main className="mx-auto max-w-3xl px-4 py-10">{children}</main>
60
+ <footer className="border-t border-zinc-200 py-6 text-center text-xs text-zinc-500">
61
+ Rendered by Pylon · one server, one port
62
+ </footer>
63
+ </body>
64
+ </html>
65
+ );
66
+ }
@@ -0,0 +1,57 @@
1
+ import React from "react";
2
+ import { Link } from "@pylonsync/react";
3
+
4
+ interface PageProps {
5
+ url: string;
6
+ }
7
+
8
+ // `app/page.tsx` → `/`. Pages receive `{ url, auth, searchParams }` from
9
+ // the SSR runtime. This renders to HTML on the server; the per-route
10
+ // chunk hydrates it in the browser so interactive pages (see /counter)
11
+ // just work.
12
+ export default function IndexPage({ url }: PageProps) {
13
+ return (
14
+ <div className="space-y-8">
15
+ <section>
16
+ <h1 className="text-3xl font-semibold tracking-tight">__APP_NAME__</h1>
17
+ <p className="mt-2 text-zinc-600">
18
+ A full-stack Pylon app. Server-rendered React, file-based routes,
19
+ a synced database, and a typed client — served from one binary on
20
+ one port. No Next.js, no separate API server.
21
+ </p>
22
+ </section>
23
+
24
+ <section className="rounded-2xl border border-zinc-200 bg-white p-6">
25
+ <h2 className="text-lg font-semibold">Next steps</h2>
26
+ <ul className="mt-3 space-y-2 text-sm text-zinc-700">
27
+ <li>
28
+ Add a route: drop a file at{" "}
29
+ <code className="rounded bg-zinc-100 px-1">app/about/page.tsx</code>{" "}
30
+ and visit <code className="rounded bg-zinc-100 px-1">/about</code>.
31
+ </li>
32
+ <li>
33
+ Add data: edit{" "}
34
+ <code className="rounded bg-zinc-100 px-1">app.ts</code> — every{" "}
35
+ <code className="rounded bg-zinc-100 px-1">entity()</code> gets a
36
+ REST + realtime API and a typed client automatically.
37
+ </li>
38
+ <li>
39
+ See hydration in action on{" "}
40
+ <Link
41
+ href="/counter"
42
+ className="text-blue-600 underline-offset-4 hover:underline"
43
+ >
44
+ /counter
45
+ </Link>
46
+ .
47
+ </li>
48
+ </ul>
49
+ </section>
50
+
51
+ <p className="text-xs text-zinc-500">
52
+ You're at <code>{url}</code>. Edit{" "}
53
+ <code>app/page.tsx</code> and save — the page reloads instantly.
54
+ </p>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,32 @@
1
+ import { buildManifest, discoverAppRoutes, entity, field } from "@pylonsync/sdk";
2
+
3
+ // Your data model. Every `entity()` becomes a synced table with a REST +
4
+ // realtime API and a typed client — no migrations to write, no resolvers
5
+ // to wire. Add fields here and they show up everywhere.
6
+ const Note = entity("Note", {
7
+ body: field.string(),
8
+ done: field.boolean().default(false),
9
+ });
10
+
11
+ // The manifest is your whole app in one object: data, server functions,
12
+ // access policies, and the file-based routes under `app/`. `pylon dev`
13
+ // reads this, serves the SSR frontend and the API from one port, and
14
+ // regenerates a typed client on every change.
15
+ //
16
+ // File-based routing: `discoverAppRoutes()` walks `app/**/page.tsx` and
17
+ // emits one route per page. Drop `app/about/page.tsx` to add `/about` —
18
+ // no route table to maintain.
19
+ const manifest = buildManifest({
20
+ name: "__APP_NAME__",
21
+ version: "0.1.0",
22
+ entities: [Note],
23
+ queries: [],
24
+ actions: [],
25
+ policies: [],
26
+ routes: await discoverAppRoutes(),
27
+ });
28
+
29
+ // Emit canonical manifest JSON to stdout for `pylon codegen`.
30
+ console.log(JSON.stringify(manifest, null, 2));
31
+
32
+ export default manifest;
@@ -0,0 +1,13 @@
1
+ // Server functions go here. Each file in this directory that exports a
2
+ // query() or action() becomes a typed RPC endpoint, callable from your
3
+ // pages and client with full type inference. Delete this placeholder when
4
+ // you add your first one.
5
+ //
6
+ // Example (functions/notes.ts):
7
+ //
8
+ // import { query } from "@pylonsync/functions";
9
+ //
10
+ // export const listNotes = query(async (ctx) => {
11
+ // return ctx.db.list("Note");
12
+ // });
13
+ export {};
@@ -0,0 +1,10 @@
1
+ node_modules/
2
+ .pylon/
3
+ pylon.manifest.json
4
+ pylon.client.ts
5
+ web/dist/
6
+ *.db
7
+ *.db-*
8
+ .env
9
+ .env.local
10
+ .DS_Store
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "__APP_NAME_KEBAB__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "pylon dev",
8
+ "deploy": "pylon deploy",
9
+ "check": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@pylonsync/react": "^__PYLON_VERSION__",
13
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
14
+ "@pylonsync/functions": "^__PYLON_VERSION__",
15
+ "react": "^19.0.0",
16
+ "react-dom": "^19.0.0",
17
+ "tailwindcss": "^4.3.0",
18
+ "@tailwindcss/cli": "^4.3.0"
19
+ },
20
+ "devDependencies": {
21
+ "@pylonsync/cli": "^__PYLON_VERSION__",
22
+ "@types/react": "^19.0.0",
23
+ "@types/react-dom": "^19.0.0",
24
+ "typescript": "^5.6.0"
25
+ }
26
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react",
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "lib": ["ES2022", "DOM"],
11
+ "types": ["react", "react-dom"]
12
+ },
13
+ "include": ["app.ts", "app/**/*"]
14
+ }