@pylonsync/create-pylon 0.3.267 → 0.3.269

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 (90) 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/README.md +43 -28
  84. package/templates/ssr/app/dashboard/dashboard-client.tsx +154 -78
  85. package/templates/ssr/app/dashboard/page.tsx +16 -60
  86. package/templates/ssr/app/layout.tsx +46 -39
  87. package/templates/ssr/app/login/page.tsx +1 -1
  88. package/templates/ssr/app/page.tsx +182 -84
  89. package/templates/ssr/app/signup/page.tsx +1 -1
  90. package/templates/ssr/app.ts +134 -46
@@ -1,114 +1,212 @@
1
1
  import React from "react";
2
2
  import { Link, type Metadata, type PageProps } from "@pylonsync/react";
3
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
4
 
12
- // SEO metadata. Export `metadata` (static) or `generateMetadata(props)`
13
- // (dynamic) from any page or layoutPylon renders the <title>/<meta>
14
- // into <head> server-side. The `Metadata` type is exported from
15
- // @pylonsync/react.
5
+ // SEO metadata. Exported `metadata` is rendered into <head> on the server, so
6
+ // this marketing page is fully indexable view source and the copy is in the
7
+ // HTML. Swap "Acme" for your product throughout.
16
8
  export const metadata: Metadata = {
17
- title: "__APP_NAME__full-stack Pylon app",
9
+ title: "Acmethe workspace your team actually wants",
18
10
  description:
19
- "A server-rendered homepage, email/password auth, and a live client dashboard over one synced backend one binary, one port.",
11
+ "Acme is a collaborative workspace for fast-moving teams. Organize projects by team, collaborate in real time, and keep every tenant's data private. Built on Pylon.",
20
12
  };
21
13
 
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) {
14
+ // `app/page.tsx` → `/`. A server-rendered marketing landing page. It reads
15
+ // `auth` (resolved from the session cookie during the render) so the call to
16
+ // action is right on the first byte "Get started" for visitors, "Open
17
+ // dashboard" once you're signed in. No client fetch, no flash.
18
+ export default function LandingPage({ auth }: PageProps) {
29
19
  const signedIn = Boolean(auth.user_id);
20
+
30
21
  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>
22
+ <div className="space-y-24 pb-24">
23
+ {/* Hero */}
24
+ <section className="relative overflow-hidden">
25
+ {/* Soft gradient wash behind the hero. */}
26
+ <div
27
+ aria-hidden
28
+ className="pointer-events-none absolute -top-40 left-1/2 h-[32rem] w-[60rem] -translate-x-1/2 rounded-full bg-gradient-to-tr from-primary/15 via-primary/5 to-transparent blur-3xl"
29
+ />
30
+ <div className="relative mx-auto max-w-3xl px-2 pt-16 text-center sm:pt-24">
31
+ <span className="inline-flex items-center gap-2 rounded-full border bg-background/60 px-3 py-1 text-xs font-medium text-muted-foreground backdrop-blur">
32
+ <span className="size-1.5 rounded-full bg-emerald-500" />
33
+ New · Real-time projects for every team
34
+ </span>
35
+ <h1 className="mt-6 text-balance text-5xl font-semibold tracking-tight sm:text-6xl">
36
+ The workspace your team{" "}
37
+ <span className="bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
38
+ actually wants
39
+ </span>
40
+ .
41
+ </h1>
42
+ <p className="mx-auto mt-5 max-w-xl text-balance text-lg text-muted-foreground">
43
+ Acme keeps your team&apos;s projects organized, in sync, and private
44
+ to your organization. Invite your teammates, switch between orgs, and
45
+ watch every change land live.
46
+ </p>
47
+ <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
48
+ {signedIn ? (
49
+ <Button asChild size="lg">
50
+ <Link href="/dashboard">Open dashboard →</Link>
54
51
  </Button>
55
- <Button asChild variant="outline">
56
- <Link href="/login">Sign in</Link>
57
- </Button>
58
- </>
59
- )}
52
+ ) : (
53
+ <>
54
+ <Button asChild size="lg">
55
+ <Link href="/signup">Get started — it&apos;s free</Link>
56
+ </Button>
57
+ <Button asChild size="lg" variant="outline">
58
+ <Link href="/login">Sign in</Link>
59
+ </Button>
60
+ </>
61
+ )}
62
+ </div>
63
+ <p className="mt-4 text-xs text-muted-foreground">
64
+ No credit card required · Set up your first org in seconds.
65
+ </p>
66
+ </div>
67
+
68
+ {/* Product peek — a stylized dashboard mock so the hero has a subject. */}
69
+ <div className="relative mx-auto mt-14 max-w-4xl px-4">
70
+ <div className="rounded-xl border bg-card shadow-2xl shadow-primary/5">
71
+ <div className="flex items-center gap-1.5 border-b px-4 py-3">
72
+ <span className="size-3 rounded-full bg-red-400/70" />
73
+ <span className="size-3 rounded-full bg-yellow-400/70" />
74
+ <span className="size-3 rounded-full bg-green-400/70" />
75
+ <span className="ml-3 text-xs text-muted-foreground">
76
+ acme.app/dashboard
77
+ </span>
78
+ </div>
79
+ <div className="grid gap-4 p-5 sm:grid-cols-[1fr_1.4fr]">
80
+ <div className="space-y-2">
81
+ <div className="text-xs font-medium text-muted-foreground">
82
+ Organization
83
+ </div>
84
+ <div className="flex items-center gap-2 rounded-md border px-3 py-2 text-sm">
85
+ <span className="flex size-6 items-center justify-center rounded bg-primary/10 text-xs font-semibold text-primary">
86
+ A
87
+ </span>
88
+ Acme Inc
89
+ </div>
90
+ <div className="pt-2 text-xs font-medium text-muted-foreground">
91
+ Members
92
+ </div>
93
+ {["you · owner", "jordan · admin", "sam · member"].map((m) => (
94
+ <div
95
+ key={m}
96
+ className="rounded-md border px-3 py-1.5 font-mono text-xs text-muted-foreground"
97
+ >
98
+ {m}
99
+ </div>
100
+ ))}
101
+ </div>
102
+ <div className="space-y-2">
103
+ <div className="text-xs font-medium text-muted-foreground">
104
+ Projects
105
+ </div>
106
+ {["Website redesign", "Mobile app", "Q3 launch"].map((p, i) => (
107
+ <div
108
+ key={p}
109
+ className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"
110
+ >
111
+ {p}
112
+ {i === 0 && (
113
+ <span className="rounded bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-medium text-emerald-600">
114
+ live
115
+ </span>
116
+ )}
117
+ </div>
118
+ ))}
119
+ <div className="rounded-md border border-dashed px-3 py-2 text-sm text-muted-foreground">
120
+ + New project
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </div>
60
125
  </div>
61
126
  </section>
62
127
 
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>
128
+ {/* Logos / social proof */}
129
+ <section className="mx-auto max-w-3xl px-4 text-center">
130
+ <p className="text-xs font-medium uppercase tracking-widest text-muted-foreground">
131
+ Trusted by teams at
132
+ </p>
133
+ <div className="mt-4 flex flex-wrap items-center justify-center gap-x-10 gap-y-3 text-lg font-semibold text-muted-foreground/60">
134
+ <span>Globex</span>
135
+ <span>Initech</span>
136
+ <span>Hooli</span>
137
+ <span>Soylent</span>
138
+ <span>Stark</span>
139
+ </div>
81
140
  </section>
82
141
 
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>
142
+ {/* Features */}
143
+ <section className="mx-auto max-w-4xl px-4">
144
+ <div className="mx-auto max-w-2xl text-center">
145
+ <h2 className="text-3xl font-semibold tracking-tight">
146
+ Everything a growing team needs
147
+ </h2>
148
+ <p className="mt-3 text-muted-foreground">
149
+ Acme is multi-tenant from day one — every org is an isolated,
150
+ real-time workspace.
151
+ </p>
152
+ </div>
153
+ <div className="mt-12 grid gap-6 sm:grid-cols-3">
154
+ <Feature title="Organize by org" icon="▦">
155
+ Spin up an organization, invite your team, and switch between them in
156
+ a click. Each org&apos;s projects and members are completely private.
157
+ </Feature>
158
+ <Feature title="Real-time by default" icon="✦">
159
+ Changes sync instantly across everyone&apos;s screens — no refresh,
160
+ no stale data. Open two tabs and watch it happen.
161
+ </Feature>
162
+ <Feature title="Secure tenant isolation" icon="◆">
163
+ Access is enforced at the data layer. A member of one org can&apos;t
164
+ read or write another&apos;s rows — not by convention, by policy.
165
+ </Feature>
166
+ </div>
167
+ </section>
168
+
169
+ {/* CTA */}
170
+ <section className="mx-auto max-w-3xl px-4">
171
+ <div className="rounded-2xl border bg-gradient-to-br from-primary/10 to-transparent px-8 py-12 text-center">
172
+ <h2 className="text-3xl font-semibold tracking-tight">
173
+ Ready to get your team in sync?
174
+ </h2>
175
+ <p className="mx-auto mt-3 max-w-md text-muted-foreground">
176
+ Create your organization and invite your first teammate in under a
177
+ minute.
178
+ </p>
179
+ <div className="mt-6">
180
+ <Button asChild size="lg">
181
+ <Link href={signedIn ? "/dashboard" : "/signup"}>
182
+ {signedIn ? "Open dashboard →" : "Start for free →"}
183
+ </Link>
184
+ </Button>
185
+ </div>
186
+ </div>
187
+ </section>
87
188
  </div>
88
189
  );
89
190
  }
90
191
 
91
192
  function Feature({
92
193
  title,
194
+ icon,
93
195
  children,
94
196
  }: {
95
197
  title: string;
198
+ icon: string;
96
199
  children: React.ReactNode;
97
200
  }) {
98
201
  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>
202
+ <div className="rounded-xl border bg-card p-5">
203
+ <div className="flex size-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
204
+ {icon}
205
+ </div>
206
+ <h3 className="mt-4 text-base font-semibold tracking-tight">{title}</h3>
207
+ <p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">
208
+ {children}
209
+ </p>
210
+ </div>
109
211
  );
110
212
  }
111
-
112
- function Code({ children }: { children: React.ReactNode }) {
113
- return <code className="rounded bg-muted px-1 text-xs">{children}</code>;
114
- }
@@ -10,7 +10,7 @@ import {
10
10
  import { AuthForm } from "../auth-form";
11
11
 
12
12
  export const metadata: Metadata = {
13
- title: "Create your account — __APP_NAME__",
13
+ title: "Create your account — Acme",
14
14
  robots: "noindex",
15
15
  };
16
16
 
@@ -7,56 +7,104 @@ import {
7
7
  discoverAppRoutes,
8
8
  } from "@pylonsync/sdk";
9
9
 
10
- // Accounts. Email/password auth is built in: POST /api/auth/password/register
11
- // hashes the password and writes this row; /api/auth/password/login mints a
12
- // session and sets an HttpOnly cookie. The framework treats the entity named
13
- // "User" as the account table — `passwordHash` is server-only and never
14
- // serialized to a client.
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.
15
13
  const User = entity(
16
14
  "User",
17
15
  {
18
16
  email: field.string(),
19
17
  displayName: field.string().optional(),
20
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(),
21
22
  createdAt: field.datetime().defaultNow(),
22
23
  },
23
24
  { indexes: [{ name: "by_email", fields: ["email"], unique: true }] },
24
25
  );
25
26
 
26
- // A note that belongs to one user. `ownerId: field.owner()` is the key move:
27
- // the framework stamps the signed-in user's id server-side on insert and
28
- // rejects any forged value so the dashboard can do a plain, optimistic
29
- // `db.insert("Note", { body })` (the row shows instantly, no round-trip) while
30
- // the owner stays unspoofable. No createNote function to write.
31
- const Note = entity(
32
- "Note",
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",
33
37
  {
34
- ownerId: field.string().owner(),
35
- body: field.string(),
36
- done: field.boolean().default(false),
37
- createdAt: field.datetime().defaultNow(),
38
+ name: field.string(),
39
+ createdBy: field.id("User"),
40
+ createdAt: field.datetime(),
38
41
  },
39
- { indexes: [{ name: "by_owner", fields: ["ownerId"], unique: false }] },
42
+ { indexes: [{ name: "by_created_by", fields: ["createdBy"], unique: false }] },
40
43
  );
41
44
 
42
- // Notes are private every read and write is gated to the owner. An entity
43
- // with NO policy is denied to clients by default, so this is exactly what
44
- // makes the dashboard's live query + optimistic writes work, and only for
45
- // your own rows. `auth.userId` is the session user; `data.ownerId` is the row.
46
- const notePolicy = policy({
47
- name: "note_access",
48
- entity: "Note",
49
- allowRead: "auth.userId == data.ownerId",
50
- allowInsert: "auth.userId != null",
51
- allowUpdate: "auth.userId == data.ownerId",
52
- allowDelete: "auth.userId == data.ownerId",
53
- });
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
+ );
54
104
 
55
- // User rows are read-only to clients, and only your own (so the dashboard can
56
- // read your display name). The auth subsystem owns writes — registration and
57
- // login go through /api/auth/password/*, never the entity API.
105
+ // User rows: read your own; the auth subsystem owns writes.
58
106
  const userPolicy = policy({
59
- name: "user_access",
107
+ name: "user_self",
60
108
  entity: "User",
61
109
  allowRead: "auth.userId == data.id",
62
110
  allowInsert: "false",
@@ -64,28 +112,68 @@ const userPolicy = policy({
64
112
  allowDelete: "false",
65
113
  });
66
114
 
67
- // The manifest is your whole app in one object: data, policies, and the
68
- // file-based routes under `app/`. `pylon dev` reads this, serves the SSR
69
- // frontend and the API from one port, and regenerates a typed client on
70
- // every change.
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
+
71
157
  const manifest = buildManifest({
72
158
  name: "__APP_NAME__",
73
159
  version: "0.1.0",
74
- entities: [User, Note],
160
+ entities: [User, Org, OrgMember, OrgInvite, Project],
75
161
  queries: [],
76
162
  actions: [],
77
- policies: [userPolicy, notePolicy],
78
- // Email/password is on by default against the User entity above. `auth()`
79
- // is the knob for session lifetime, exposed fields, orgs, and trusted
80
- // origins — `auth({ session: { expiresIn: 60 * 60 * 24 * 7 } })` for a
81
- // 7-day session, etc.
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.
82
173
  auth: auth(),
83
- // File-based routing: `discoverAppRoutes()` walks `app/**/page.tsx` and
84
- // emits one route per page. Drop `app/about/page.tsx` to add `/about`.
85
174
  routes: await discoverAppRoutes(),
86
175
  });
87
176
 
88
- // Emit canonical manifest JSON to stdout for `pylon codegen`.
89
177
  console.log(JSON.stringify(manifest, null, 2));
90
178
 
91
179
  export default manifest;