@pylonsync/create-pylon 0.3.267 → 0.3.268

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.
Files changed (83) hide show
  1. package/bin/create-pylon.js +18 -10
  2. package/package.json +1 -1
  3. package/templates/b2b/AGENTS.md +61 -0
  4. package/templates/b2b/README.md +62 -0
  5. package/templates/b2b/app/auth-form.tsx +142 -0
  6. package/templates/b2b/app/dashboard/dashboard-client.tsx +192 -0
  7. package/templates/b2b/app/dashboard/page.tsx +63 -0
  8. package/templates/b2b/app/error.tsx +43 -0
  9. package/templates/b2b/app/globals.css +139 -0
  10. package/templates/b2b/app/layout.tsx +71 -0
  11. package/templates/b2b/app/login/page.tsx +47 -0
  12. package/templates/b2b/app/not-found.tsx +29 -0
  13. package/templates/b2b/app/page.tsx +114 -0
  14. package/templates/b2b/app/robots.ts +12 -0
  15. package/templates/b2b/app/signup/page.tsx +44 -0
  16. package/templates/b2b/app/sitemap.ts +27 -0
  17. package/templates/b2b/app.ts +179 -0
  18. package/templates/b2b/components/ui/button.tsx +56 -0
  19. package/templates/b2b/components/ui/card.tsx +90 -0
  20. package/templates/b2b/components.json +20 -0
  21. package/templates/b2b/functions/_keep.ts +13 -0
  22. package/templates/b2b/gitignore +10 -0
  23. package/templates/b2b/lib/utils.ts +10 -0
  24. package/templates/b2b/package.json +33 -0
  25. package/templates/b2b/tsconfig.json +18 -0
  26. package/templates/barebones/AGENTS.md +61 -0
  27. package/templates/barebones/README.md +45 -0
  28. package/templates/barebones/app/error.tsx +43 -0
  29. package/templates/barebones/app/globals.css +139 -0
  30. package/templates/barebones/app/items-client.tsx +96 -0
  31. package/templates/barebones/app/layout.tsx +27 -0
  32. package/templates/barebones/app/not-found.tsx +29 -0
  33. package/templates/barebones/app/page.tsx +28 -0
  34. package/templates/barebones/app/robots.ts +12 -0
  35. package/templates/barebones/app/sitemap.ts +27 -0
  36. package/templates/barebones/app.ts +55 -0
  37. package/templates/barebones/components/ui/button.tsx +56 -0
  38. package/templates/barebones/components/ui/card.tsx +90 -0
  39. package/templates/barebones/components.json +20 -0
  40. package/templates/barebones/functions/_keep.ts +13 -0
  41. package/templates/barebones/gitignore +10 -0
  42. package/templates/barebones/lib/utils.ts +10 -0
  43. package/templates/barebones/package.json +33 -0
  44. package/templates/barebones/tsconfig.json +18 -0
  45. package/templates/chat/AGENTS.md +61 -0
  46. package/templates/chat/README.md +51 -0
  47. package/templates/chat/app/chat-client.tsx +113 -0
  48. package/templates/chat/app/error.tsx +43 -0
  49. package/templates/chat/app/globals.css +139 -0
  50. package/templates/chat/app/layout.tsx +25 -0
  51. package/templates/chat/app/not-found.tsx +29 -0
  52. package/templates/chat/app/page.tsx +26 -0
  53. package/templates/chat/app/robots.ts +12 -0
  54. package/templates/chat/app/sitemap.ts +27 -0
  55. package/templates/chat/app.ts +59 -0
  56. package/templates/chat/components/ui/button.tsx +56 -0
  57. package/templates/chat/components/ui/card.tsx +90 -0
  58. package/templates/chat/components.json +20 -0
  59. package/templates/chat/functions/_keep.ts +13 -0
  60. package/templates/chat/gitignore +10 -0
  61. package/templates/chat/lib/utils.ts +10 -0
  62. package/templates/chat/package.json +33 -0
  63. package/templates/chat/tsconfig.json +18 -0
  64. package/templates/consumer/AGENTS.md +61 -0
  65. package/templates/consumer/README.md +52 -0
  66. package/templates/consumer/app/error.tsx +43 -0
  67. package/templates/consumer/app/feed-client.tsx +154 -0
  68. package/templates/consumer/app/globals.css +139 -0
  69. package/templates/consumer/app/layout.tsx +27 -0
  70. package/templates/consumer/app/not-found.tsx +29 -0
  71. package/templates/consumer/app/page.tsx +27 -0
  72. package/templates/consumer/app/robots.ts +12 -0
  73. package/templates/consumer/app/sitemap.ts +27 -0
  74. package/templates/consumer/app.ts +89 -0
  75. package/templates/consumer/components/ui/button.tsx +56 -0
  76. package/templates/consumer/components/ui/card.tsx +90 -0
  77. package/templates/consumer/components.json +20 -0
  78. package/templates/consumer/functions/_keep.ts +13 -0
  79. package/templates/consumer/gitignore +10 -0
  80. package/templates/consumer/lib/utils.ts +10 -0
  81. package/templates/consumer/package.json +33 -0
  82. package/templates/consumer/tsconfig.json +18 -0
  83. package/templates/ssr/app.ts +3 -0
@@ -0,0 +1,139 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ /* Tailwind v4 scans these globs for class names. `components/` is here so
5
+ shadcn/ui component classes are seen — add more @source lines if you put
6
+ markup elsewhere. */
7
+ @source "../app/**/*.{tsx,ts,jsx,js}";
8
+ @source "../components/**/*.{tsx,ts,jsx,js}";
9
+
10
+ @custom-variant dark (&:where(.dark, .dark *));
11
+
12
+ /* shadcn/ui design tokens (new-york / zinc). Edit these to re-theme the
13
+ whole app; `npx shadcn@latest add <component>` drops new components that
14
+ consume the same variables. Toggle dark mode by putting `class="dark"`
15
+ on <html>. */
16
+ :root {
17
+ --radius: 0.625rem;
18
+ --background: oklch(1 0 0);
19
+ --foreground: oklch(0.141 0.005 285.823);
20
+ --card: oklch(1 0 0);
21
+ --card-foreground: oklch(0.141 0.005 285.823);
22
+ --popover: oklch(1 0 0);
23
+ --popover-foreground: oklch(0.141 0.005 285.823);
24
+ --primary: oklch(0.21 0.006 285.885);
25
+ --primary-foreground: oklch(0.985 0 0);
26
+ --secondary: oklch(0.967 0.001 286.375);
27
+ --secondary-foreground: oklch(0.21 0.006 285.885);
28
+ --muted: oklch(0.967 0.001 286.375);
29
+ --muted-foreground: oklch(0.552 0.016 285.938);
30
+ --accent: oklch(0.967 0.001 286.375);
31
+ --accent-foreground: oklch(0.21 0.006 285.885);
32
+ --destructive: oklch(0.577 0.245 27.325);
33
+ --border: oklch(0.92 0.004 286.32);
34
+ --input: oklch(0.92 0.004 286.32);
35
+ --ring: oklch(0.705 0.015 286.067);
36
+ --chart-1: oklch(0.646 0.222 41.116);
37
+ --chart-2: oklch(0.6 0.118 184.704);
38
+ --chart-3: oklch(0.398 0.07 227.392);
39
+ --chart-4: oklch(0.828 0.189 84.429);
40
+ --chart-5: oklch(0.769 0.188 70.08);
41
+ --sidebar: oklch(0.985 0 0);
42
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
43
+ --sidebar-primary: oklch(0.21 0.006 285.885);
44
+ --sidebar-primary-foreground: oklch(0.985 0 0);
45
+ --sidebar-accent: oklch(0.967 0.001 286.375);
46
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
47
+ --sidebar-border: oklch(0.92 0.004 286.32);
48
+ --sidebar-ring: oklch(0.705 0.015 286.067);
49
+ }
50
+
51
+ .dark {
52
+ --background: oklch(0.141 0.005 285.823);
53
+ --foreground: oklch(0.985 0 0);
54
+ --card: oklch(0.21 0.006 285.885);
55
+ --card-foreground: oklch(0.985 0 0);
56
+ --popover: oklch(0.21 0.006 285.885);
57
+ --popover-foreground: oklch(0.985 0 0);
58
+ --primary: oklch(0.92 0.004 286.32);
59
+ --primary-foreground: oklch(0.21 0.006 285.885);
60
+ --secondary: oklch(0.274 0.006 286.033);
61
+ --secondary-foreground: oklch(0.985 0 0);
62
+ --muted: oklch(0.274 0.006 286.033);
63
+ --muted-foreground: oklch(0.705 0.015 286.067);
64
+ --accent: oklch(0.274 0.006 286.033);
65
+ --accent-foreground: oklch(0.985 0 0);
66
+ --destructive: oklch(0.704 0.191 22.216);
67
+ --border: oklch(1 0 0 / 10%);
68
+ --input: oklch(1 0 0 / 15%);
69
+ --ring: oklch(0.552 0.016 285.938);
70
+ --chart-1: oklch(0.488 0.243 264.376);
71
+ --chart-2: oklch(0.696 0.17 162.48);
72
+ --chart-3: oklch(0.769 0.188 70.08);
73
+ --chart-4: oklch(0.627 0.265 303.9);
74
+ --chart-5: oklch(0.645 0.246 16.439);
75
+ --sidebar: oklch(0.21 0.006 285.885);
76
+ --sidebar-foreground: oklch(0.985 0 0);
77
+ --sidebar-primary: oklch(0.488 0.243 264.376);
78
+ --sidebar-primary-foreground: oklch(0.985 0 0);
79
+ --sidebar-accent: oklch(0.274 0.006 286.033);
80
+ --sidebar-accent-foreground: oklch(0.985 0 0);
81
+ --sidebar-border: oklch(1 0 0 / 10%);
82
+ --sidebar-ring: oklch(0.552 0.016 285.938);
83
+ }
84
+
85
+ @theme inline {
86
+ --radius-sm: calc(var(--radius) - 4px);
87
+ --radius-md: calc(var(--radius) - 2px);
88
+ --radius-lg: var(--radius);
89
+ --radius-xl: calc(var(--radius) + 4px);
90
+ --color-background: var(--background);
91
+ --color-foreground: var(--foreground);
92
+ --color-card: var(--card);
93
+ --color-card-foreground: var(--card-foreground);
94
+ --color-popover: var(--popover);
95
+ --color-popover-foreground: var(--popover-foreground);
96
+ --color-primary: var(--primary);
97
+ --color-primary-foreground: var(--primary-foreground);
98
+ --color-secondary: var(--secondary);
99
+ --color-secondary-foreground: var(--secondary-foreground);
100
+ --color-muted: var(--muted);
101
+ --color-muted-foreground: var(--muted-foreground);
102
+ --color-accent: var(--accent);
103
+ --color-accent-foreground: var(--accent-foreground);
104
+ --color-destructive: var(--destructive);
105
+ --color-border: var(--border);
106
+ --color-input: var(--input);
107
+ --color-ring: var(--ring);
108
+ --color-chart-1: var(--chart-1);
109
+ --color-chart-2: var(--chart-2);
110
+ --color-chart-3: var(--chart-3);
111
+ --color-chart-4: var(--chart-4);
112
+ --color-chart-5: var(--chart-5);
113
+ --color-sidebar: var(--sidebar);
114
+ --color-sidebar-foreground: var(--sidebar-foreground);
115
+ --color-sidebar-primary: var(--sidebar-primary);
116
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
117
+ --color-sidebar-accent: var(--sidebar-accent);
118
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
119
+ --color-sidebar-border: var(--sidebar-border);
120
+ --color-sidebar-ring: var(--sidebar-ring);
121
+ }
122
+
123
+ @layer base {
124
+ *,
125
+ ::after,
126
+ ::before,
127
+ ::backdrop,
128
+ ::file-selector-button {
129
+ border-color: var(--color-border, currentColor);
130
+ outline-color: var(--color-ring);
131
+ }
132
+ body {
133
+ background-color: var(--color-background);
134
+ color: var(--color-foreground);
135
+ }
136
+ button {
137
+ cursor: pointer;
138
+ }
139
+ }
@@ -0,0 +1,71 @@
1
+ import React from "react";
2
+ import { Link, type PageAuth } from "@pylonsync/react";
3
+
4
+ // A layout receives the page props plus `children`. `auth.user_id` is null
5
+ // for anonymous visitors and the signed-in user's id otherwise — resolved
6
+ // server-side from the session cookie, before any HTML is sent, so the nav
7
+ // renders the right links on the first byte (no flash, no client fetch). The
8
+ // `PageAuth` type is exported from @pylonsync/react so you never hand-roll it.
9
+ interface LayoutProps {
10
+ children: React.ReactNode;
11
+ url: string;
12
+ auth: PageAuth;
13
+ }
14
+
15
+ // The root layout wraps every page.
16
+ export default function RootLayout({ children, auth }: LayoutProps) {
17
+ const signedIn = Boolean(auth?.user_id);
18
+ // Add `className="dark"` to this <html> to flip every shadcn token to its
19
+ // dark value. The classes below use semantic tokens (bg-background,
20
+ // text-foreground, …) so the whole UI re-themes from app/globals.css.
21
+ return (
22
+ <html lang="en">
23
+ <head>
24
+ <meta charSet="utf-8" />
25
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
26
+ <title>__APP_NAME__</title>
27
+ {/* Tailwind is compiled by Pylon from app/globals.css and the
28
+ stylesheet link is injected here automatically — nothing to
29
+ wire up. */}
30
+ </head>
31
+ <body className="min-h-screen bg-background text-foreground antialiased">
32
+ <header className="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
33
+ <div className="mx-auto flex max-w-3xl items-center justify-between px-4 py-3">
34
+ <Link
35
+ href="/"
36
+ className="text-sm font-semibold tracking-tight hover:text-muted-foreground"
37
+ >
38
+ __APP_NAME__
39
+ </Link>
40
+ <nav className="flex items-center gap-4 text-sm text-muted-foreground">
41
+ <Link href="/" className="hover:text-foreground">
42
+ Home
43
+ </Link>
44
+ {signedIn ? (
45
+ <Link href="/dashboard" className="hover:text-foreground">
46
+ Dashboard
47
+ </Link>
48
+ ) : (
49
+ <>
50
+ <Link href="/login" className="hover:text-foreground">
51
+ Sign in
52
+ </Link>
53
+ <Link
54
+ href="/signup"
55
+ className="rounded-md bg-primary px-3 py-1.5 font-medium text-primary-foreground hover:opacity-90"
56
+ >
57
+ Sign up
58
+ </Link>
59
+ </>
60
+ )}
61
+ </nav>
62
+ </div>
63
+ </header>
64
+ <main className="mx-auto max-w-3xl px-4 py-10">{children}</main>
65
+ <footer className="border-t py-6 text-center text-xs text-muted-foreground">
66
+ Rendered by Pylon · one server, one port
67
+ </footer>
68
+ </body>
69
+ </html>
70
+ );
71
+ }
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+ import { Link, type Metadata, type PageProps } from "@pylonsync/react";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "@/components/ui/card";
10
+ import { AuthForm } from "../auth-form";
11
+
12
+ export const metadata: Metadata = {
13
+ title: "Sign in — __APP_NAME__",
14
+ // Auth pages shouldn't be indexed.
15
+ robots: "noindex",
16
+ };
17
+
18
+ // `app/login/page.tsx` → `/login`. A server-rendered shell around the
19
+ // client-side <AuthForm> island.
20
+ export default function LoginPage({ auth, response }: PageProps) {
21
+ // Already signed in? Skip the form. `response.redirect` runs in the
22
+ // synchronous shell render, so it's a real 307 before any HTML is sent
23
+ // (no flash, works with JS disabled).
24
+ if (auth.user_id) response.redirect("/dashboard");
25
+ return (
26
+ <div className="mx-auto max-w-sm">
27
+ <Card>
28
+ <CardHeader>
29
+ <CardTitle>Sign in</CardTitle>
30
+ <CardDescription>Welcome back.</CardDescription>
31
+ </CardHeader>
32
+ <CardContent className="space-y-4">
33
+ <AuthForm mode="login" />
34
+ <p className="text-center text-sm text-muted-foreground">
35
+ No account?{" "}
36
+ <Link
37
+ href="/signup"
38
+ className="font-medium text-foreground hover:underline"
39
+ >
40
+ Create one
41
+ </Link>
42
+ </p>
43
+ </CardContent>
44
+ </Card>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import { Link, useRouter, type NotFoundProps } from "@pylonsync/react";
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ // `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL (and when
6
+ // a page calls `response.notFound()`). It's HYDRATED, so it's interactive:
7
+ // the buttons below use the client router. Not-found boundaries receive the
8
+ // standard page props (and, matching Next, no `reset`).
9
+ export default function NotFound(_props: NotFoundProps) {
10
+ const router = useRouter();
11
+ return (
12
+ <div className="space-y-6">
13
+ <section>
14
+ <h1 className="text-2xl font-semibold tracking-tight">404</h1>
15
+ <p className="mt-2 text-muted-foreground">
16
+ We couldn&apos;t find that page.
17
+ </p>
18
+ </section>
19
+ <div className="flex items-center gap-3">
20
+ <Button onClick={() => router.back()} variant="outline">
21
+ ← Go back
22
+ </Button>
23
+ <Button asChild>
24
+ <Link href="/">Home</Link>
25
+ </Button>
26
+ </div>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,114 @@
1
+ import React from "react";
2
+ import { Link, type Metadata, type PageProps } from "@pylonsync/react";
3
+ import { Button } from "@/components/ui/button";
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from "@/components/ui/card";
11
+
12
+ // SEO metadata. Export `metadata` (static) or `generateMetadata(props)`
13
+ // (dynamic) from any page or layout — Pylon renders the <title>/<meta>
14
+ // into <head> server-side. The `Metadata` type is exported from
15
+ // @pylonsync/react.
16
+ export const metadata: Metadata = {
17
+ title: "__APP_NAME__ — full-stack Pylon app",
18
+ description:
19
+ "A server-rendered homepage, email/password auth, and a live client dashboard over one synced backend — one binary, one port.",
20
+ };
21
+
22
+ // `app/page.tsx` → `/`. This page is server-rendered: view source and the copy
23
+ // is in the HTML, not fetched later — good for SEO and first paint. It reads
24
+ // `auth` (resolved from the session cookie during the render) to show the
25
+ // right call to action. Every page receives `PageProps` from the SSR runtime:
26
+ // `{ url, params, searchParams, auth, response, serverData }` — typed, no
27
+ // hand-rolled interface.
28
+ export default function IndexPage({ auth }: PageProps) {
29
+ const signedIn = Boolean(auth.user_id);
30
+ return (
31
+ <div className="space-y-12">
32
+ <section className="space-y-5">
33
+ <span className="inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium text-muted-foreground">
34
+ Server-rendered · authenticated · synced · one port
35
+ </span>
36
+ <h1 className="text-4xl font-semibold tracking-tight">
37
+ Full-stack apps, one binary.
38
+ </h1>
39
+ <p className="max-w-xl text-lg text-muted-foreground">
40
+ This homepage is server-rendered React. Sign in and your dashboard
41
+ becomes a live, local-first view over the same Pylon backend — writes
42
+ appear instantly and sync across tabs. No Next.js, no separate API
43
+ server, no realtime sidecar.
44
+ </p>
45
+ <div className="flex flex-wrap items-center gap-3">
46
+ {signedIn ? (
47
+ <Button asChild>
48
+ <Link href="/dashboard">Go to your dashboard →</Link>
49
+ </Button>
50
+ ) : (
51
+ <>
52
+ <Button asChild>
53
+ <Link href="/signup">Get started</Link>
54
+ </Button>
55
+ <Button asChild variant="outline">
56
+ <Link href="/login">Sign in</Link>
57
+ </Button>
58
+ </>
59
+ )}
60
+ </div>
61
+ </section>
62
+
63
+ <section className="grid gap-4 sm:grid-cols-3">
64
+ <Feature title="Server-rendered">
65
+ File-based routes under <Code>app/</Code>. Pages render to HTML on the
66
+ server with <Code>metadata</Code> in <Code>{"<head>"}</Code>, then
67
+ hydrate. Drop <Code>app/about/page.tsx</Code> to add{" "}
68
+ <Code>/about</Code>.
69
+ </Feature>
70
+ <Feature title="Auth included">
71
+ Email/password is built in. <Code>/login</Code> and{" "}
72
+ <Code>/signup</Code> hit <Code>/api/auth/password/*</Code>; the server
73
+ sets an HttpOnly session cookie. <Code>/dashboard</Code> gates on it
74
+ server-side.
75
+ </Feature>
76
+ <Feature title="Synced database">
77
+ Every <Code>entity()</Code> in <Code>app.ts</Code> gets a REST +
78
+ realtime API and a typed client. <Code>db.useQuery</Code> is live;{" "}
79
+ <Code>db.insert</Code> is optimistic.
80
+ </Feature>
81
+ </section>
82
+
83
+ <p className="text-xs text-muted-foreground">
84
+ Edit <Code>app/page.tsx</Code> and save — the page reloads instantly.
85
+ The data model and access policies live in <Code>app.ts</Code>.
86
+ </p>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ function Feature({
92
+ title,
93
+ children,
94
+ }: {
95
+ title: string;
96
+ children: React.ReactNode;
97
+ }) {
98
+ return (
99
+ <Card>
100
+ <CardHeader>
101
+ <CardTitle className="text-base">{title}</CardTitle>
102
+ </CardHeader>
103
+ <CardContent>
104
+ <CardDescription className="text-sm leading-relaxed">
105
+ {children}
106
+ </CardDescription>
107
+ </CardContent>
108
+ </Card>
109
+ );
110
+ }
111
+
112
+ function Code({ children }: { children: React.ReactNode }) {
113
+ return <code className="rounded bg-muted px-1 text-xs">{children}</code>;
114
+ }
@@ -0,0 +1,12 @@
1
+ import type { Robots } from "@pylonsync/react";
2
+
3
+ // app/robots.ts → served at /robots.txt. The default export may also be async.
4
+ const SITE = process.env.SITE_URL ?? "http://localhost:4321";
5
+
6
+ export default function robots(): Robots {
7
+ return {
8
+ // Keep the authenticated app and the API out of the index.
9
+ rules: { userAgent: "*", allow: "/", disallow: ["/dashboard", "/api/"] },
10
+ sitemap: `${SITE}/sitemap.xml`,
11
+ };
12
+ }
@@ -0,0 +1,44 @@
1
+ import React from "react";
2
+ import { Link, type Metadata, type PageProps } from "@pylonsync/react";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "@/components/ui/card";
10
+ import { AuthForm } from "../auth-form";
11
+
12
+ export const metadata: Metadata = {
13
+ title: "Create your account — __APP_NAME__",
14
+ robots: "noindex",
15
+ };
16
+
17
+ // `app/signup/page.tsx` → `/signup`. Same shell as /login, register mode.
18
+ export default function SignupPage({ auth, response }: PageProps) {
19
+ if (auth.user_id) response.redirect("/dashboard");
20
+ return (
21
+ <div className="mx-auto max-w-sm">
22
+ <Card>
23
+ <CardHeader>
24
+ <CardTitle>Create your account</CardTitle>
25
+ <CardDescription>
26
+ Email + password. No credit card, no email verification step in dev.
27
+ </CardDescription>
28
+ </CardHeader>
29
+ <CardContent className="space-y-4">
30
+ <AuthForm mode="signup" />
31
+ <p className="text-center text-sm text-muted-foreground">
32
+ Already have an account?{" "}
33
+ <Link
34
+ href="/login"
35
+ className="font-medium text-foreground hover:underline"
36
+ >
37
+ Sign in
38
+ </Link>
39
+ </p>
40
+ </CardContent>
41
+ </Card>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,27 @@
1
+ import type { Sitemap } from "@pylonsync/react";
2
+
3
+ // app/sitemap.ts → served at /sitemap.xml. The default export can be async, so
4
+ // it can enumerate dynamic pages from your database. Point SITE_URL at your
5
+ // domain in production.
6
+ const SITE = process.env.SITE_URL ?? "http://localhost:4321";
7
+
8
+ export default async function sitemap(): Promise<Sitemap> {
9
+ // Only public pages belong here — /dashboard is private (and noindex), so
10
+ // it's intentionally left out.
11
+ const staticRoutes: Sitemap = [
12
+ { url: `${SITE}/`, changeFrequency: "weekly", priority: 1 },
13
+ { url: `${SITE}/login`, changeFrequency: "yearly", priority: 0.3 },
14
+ { url: `${SITE}/signup`, changeFrequency: "yearly", priority: 0.5 },
15
+ ];
16
+
17
+ // The export is async, so you can enumerate dynamic pages from a DB read:
18
+ //
19
+ // const posts = await fetchPublishedPosts();
20
+ // const postRoutes: Sitemap = posts.map((p) => ({
21
+ // url: `${SITE}/blog/${p.slug}`,
22
+ // lastModified: p.updatedAt,
23
+ // }));
24
+ // return [...staticRoutes, ...postRoutes];
25
+
26
+ return staticRoutes;
27
+ }
@@ -0,0 +1,179 @@
1
+ import {
2
+ entity,
3
+ field,
4
+ policy,
5
+ auth,
6
+ buildManifest,
7
+ discoverAppRoutes,
8
+ } from "@pylonsync/sdk";
9
+
10
+ // Accounts — email/password is built in (the entity named "User" is the
11
+ // account table; passwordHash is server-only). Each user can belong to many
12
+ // organizations.
13
+ const User = entity(
14
+ "User",
15
+ {
16
+ email: field.string(),
17
+ displayName: field.string().optional(),
18
+ passwordHash: field.string().serverOnly().optional(),
19
+ // The framework's /api/auth/password/register stamps a generated avatar
20
+ // color here, so the User entity must declare it.
21
+ avatarColor: field.string().optional(),
22
+ createdAt: field.datetime().defaultNow(),
23
+ },
24
+ { indexes: [{ name: "by_email", fields: ["email"], unique: true }] },
25
+ );
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Organizations — multi-tenancy is a framework primitive. Declaring these
29
+ // three entities with the names + fields below lights up the built-in
30
+ // `/api/auth/orgs/*` routes (create/list orgs, members, invites) and
31
+ // `/api/auth/select-org` (switch your active tenant). The framework writes
32
+ // only the fields it manages; add your own (logo, plan, billingEmail…) freely.
33
+ // The `@pylonsync/client` `<OrganizationSwitcher>` drives all of this for you.
34
+ // ---------------------------------------------------------------------------
35
+ const Org = entity(
36
+ "Org",
37
+ {
38
+ name: field.string(),
39
+ createdBy: field.id("User"),
40
+ createdAt: field.datetime(),
41
+ },
42
+ { indexes: [{ name: "by_created_by", fields: ["createdBy"], unique: false }] },
43
+ );
44
+
45
+ // User ↔ Org edge with a role. `select-org` checks this table before letting
46
+ // you switch tenants, so a client can't impersonate an org it doesn't belong
47
+ // to. role ∈ "owner" | "admin" | "member".
48
+ const OrgMember = entity(
49
+ "OrgMember",
50
+ {
51
+ orgId: field.id("Org"),
52
+ userId: field.id("User"),
53
+ role: field.string(),
54
+ joinedAt: field.datetime(),
55
+ },
56
+ {
57
+ indexes: [
58
+ { name: "by_org_user", fields: ["orgId", "userId"], unique: true },
59
+ { name: "by_user", fields: ["userId"], unique: false },
60
+ ],
61
+ },
62
+ );
63
+
64
+ // Pending invite. The framework's /api/auth/orgs/:id/invites endpoints write
65
+ // these (tokenHash is server-only — the raw token only ever goes to the
66
+ // invitee). accepted* are filled in when the invite is redeemed.
67
+ const OrgInvite = entity(
68
+ "OrgInvite",
69
+ {
70
+ orgId: field.id("Org"),
71
+ email: field.string(),
72
+ role: field.string(),
73
+ invitedBy: field.id("User"),
74
+ tokenHash: field.string().serverOnly(),
75
+ tokenPrefix: field.string(),
76
+ createdAt: field.datetime(),
77
+ expiresAt: field.datetime(),
78
+ acceptedAt: field.datetime().optional(),
79
+ acceptedByUserId: field.id("User").optional(),
80
+ },
81
+ {
82
+ indexes: [
83
+ { name: "by_org", fields: ["orgId"], unique: false },
84
+ { name: "by_email_org", fields: ["email", "orgId"], unique: false },
85
+ ],
86
+ },
87
+ );
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Your app's data — one tenant-scoped resource. `orgId` carries the tenant,
91
+ // and the policy scopes every read AND write to your ACTIVE org
92
+ // (`auth.tenantId`, set by select-org). Switch orgs in the UI and the project
93
+ // list changes — clients literally cannot read or write another tenant's rows.
94
+ // ---------------------------------------------------------------------------
95
+ const Project = entity(
96
+ "Project",
97
+ {
98
+ orgId: field.id("Org"),
99
+ name: field.string(),
100
+ createdAt: field.datetime().defaultNow(),
101
+ },
102
+ { indexes: [{ name: "by_org", fields: ["orgId"], unique: false }] },
103
+ );
104
+
105
+ // User rows: read your own; the auth subsystem owns writes.
106
+ const userPolicy = policy({
107
+ name: "user_self",
108
+ entity: "User",
109
+ allowRead: "auth.userId == data.id",
110
+ allowInsert: "false",
111
+ allowUpdate: "false",
112
+ allowDelete: "false",
113
+ });
114
+
115
+ // Org / OrgMember / OrgInvite are managed by the framework's /api/auth/orgs
116
+ // routes (which bypass these policies via the OrgStore). Clients reach them
117
+ // through the `@pylonsync/client` org helpers, not the entity API — so deny
118
+ // direct writes, and scope reads to your own membership / active org.
119
+ const orgPolicy = policy({
120
+ name: "org_access",
121
+ entity: "Org",
122
+ allowRead: "auth.tenantId == data.id",
123
+ allowInsert: "false",
124
+ allowUpdate: "false",
125
+ allowDelete: "false",
126
+ });
127
+ const orgMemberPolicy = policy({
128
+ name: "org_member_access",
129
+ entity: "OrgMember",
130
+ allowRead: "auth.userId == data.userId || auth.tenantId == data.orgId",
131
+ allowInsert: "false",
132
+ allowUpdate: "false",
133
+ allowDelete: "false",
134
+ });
135
+ const orgInvitePolicy = policy({
136
+ name: "org_invite_access",
137
+ entity: "OrgInvite",
138
+ allowRead: "auth.tenantId == data.orgId",
139
+ allowInsert: "false",
140
+ allowUpdate: "false",
141
+ allowDelete: "false",
142
+ });
143
+
144
+ // Projects are scoped to your ACTIVE tenant. `auth.tenantId == data.orgId`
145
+ // gates read AND write — and because orgId is client-supplied at insert time
146
+ // (not stamped later), checking it here means you can only create a project in
147
+ // the org you've selected. Switch orgs → a different project list.
148
+ const projectPolicy = policy({
149
+ name: "project_tenant",
150
+ entity: "Project",
151
+ allowRead: "auth.tenantId == data.orgId",
152
+ allowInsert: "auth.tenantId == data.orgId",
153
+ allowUpdate: "auth.tenantId == data.orgId",
154
+ allowDelete: "auth.tenantId == data.orgId",
155
+ });
156
+
157
+ const manifest = buildManifest({
158
+ name: "__APP_NAME__",
159
+ version: "0.1.0",
160
+ entities: [User, Org, OrgMember, OrgInvite, Project],
161
+ queries: [],
162
+ actions: [],
163
+ policies: [
164
+ userPolicy,
165
+ orgPolicy,
166
+ orgMemberPolicy,
167
+ orgInvitePolicy,
168
+ projectPolicy,
169
+ ],
170
+ // Email/password is on by default against the User entity. The org entities
171
+ // above are named with the framework defaults (Org / OrgMember / OrgInvite),
172
+ // so `/api/auth/orgs/*` + `/api/auth/select-org` work with no extra config.
173
+ auth: auth(),
174
+ routes: await discoverAppRoutes(),
175
+ });
176
+
177
+ console.log(JSON.stringify(manifest, null, 2));
178
+
179
+ export default manifest;