@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
@@ -0,0 +1,89 @@
1
+ import {
2
+ entity,
3
+ field,
4
+ policy,
5
+ auth,
6
+ buildManifest,
7
+ discoverAppRoutes,
8
+ } from "@pylonsync/sdk";
9
+
10
+ // A post in the public feed. `authorId: field.owner()` stamps the signed-in
11
+ // (guest) user's id server-side, so an optimistic `db.insert("Post", { text })`
12
+ // can't forge authorship. The feed itself is public-read (everyone sees every
13
+ // post) — that's intentional for a social feed, NOT the insecure wide-open
14
+ // default; writes are still owner-only.
15
+ const Post = entity(
16
+ "Post",
17
+ {
18
+ authorId: field.string().owner(),
19
+ text: field.string(),
20
+ createdAt: field.datetime().defaultNow(),
21
+ },
22
+ { indexes: [{ name: "by_created", fields: ["createdAt"], unique: false }] },
23
+ );
24
+
25
+ // A like is a join row (one per user per post). To toggle a like the client
26
+ // inserts or deletes this row; the like count for a post is just how many
27
+ // Like rows point at it. `userId: field.owner()` keeps a like attributable to
28
+ // exactly one user.
29
+ const Like = entity(
30
+ "Like",
31
+ {
32
+ userId: field.string().owner(),
33
+ postId: field.string(),
34
+ createdAt: field.datetime().defaultNow(),
35
+ },
36
+ {
37
+ indexes: [
38
+ { name: "by_post", fields: ["postId"], unique: false },
39
+ // One like per user per post — the unique index makes a double-like a
40
+ // no-op at the storage layer.
41
+ { name: "by_user_post", fields: ["userId", "postId"], unique: true },
42
+ ],
43
+ },
44
+ );
45
+
46
+ // Posts + likes are public-read so the feed and its counts render for
47
+ // everyone; writes are gated to the owner. An entity with no policy is denied
48
+ // to clients by default, so these allow-lists are what make the feed work.
49
+ // `allowInsert` is `auth.userId != null`, not `== data.authorId`: the owner
50
+ // field is stamped by field.owner() *after* the policy check, so it's null at
51
+ // insert-time. The stamp still guarantees the new row is owned by the caller,
52
+ // and read/update/delete enforce ownership on the persisted row.
53
+ const postPolicy = policy({
54
+ name: "post_feed",
55
+ entity: "Post",
56
+ allowRead: "true",
57
+ allowInsert: "auth.userId != null",
58
+ allowUpdate: "auth.userId == data.authorId",
59
+ allowDelete: "auth.userId == data.authorId",
60
+ });
61
+
62
+ const likePolicy = policy({
63
+ name: "like_access",
64
+ entity: "Like",
65
+ allowRead: "true",
66
+ allowInsert: "auth.userId != null",
67
+ allowUpdate: "false",
68
+ allowDelete: "auth.userId == data.userId",
69
+ });
70
+
71
+ // `pylon dev` serves the SSR feed and the API from one port. Guest sessions
72
+ // (via `<EnsureGuest>` on the page) let every visitor post + like with no
73
+ // login. Natural next steps: a `Profile` entity (displayName/avatar keyed by
74
+ // userId) to show names instead of ids, and a `Follow` join entity
75
+ // (followerId/followedId) to scope the feed to people you follow.
76
+ const manifest = buildManifest({
77
+ name: "__APP_NAME__",
78
+ version: "0.1.0",
79
+ entities: [Post, Like],
80
+ queries: [],
81
+ actions: [],
82
+ policies: [postPolicy, likePolicy],
83
+ auth: auth(),
84
+ routes: await discoverAppRoutes(),
85
+ });
86
+
87
+ console.log(JSON.stringify(manifest, null, 2));
88
+
89
+ export default manifest;
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-9 px-4 py-2",
24
+ sm: "h-8 rounded-md px-3 text-xs",
25
+ lg: "h-10 rounded-md px-8",
26
+ icon: "h-9 w-9",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ },
34
+ );
35
+
36
+ export interface ButtonProps
37
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
38
+ VariantProps<typeof buttonVariants> {
39
+ asChild?: boolean;
40
+ }
41
+
42
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
43
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
44
+ const Comp = asChild ? Slot : "button";
45
+ return (
46
+ <Comp
47
+ className={cn(buttonVariants({ variant, size, className }))}
48
+ ref={ref}
49
+ {...props}
50
+ />
51
+ );
52
+ },
53
+ );
54
+ Button.displayName = "Button";
55
+
56
+ export { Button, buttonVariants };
@@ -0,0 +1,90 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "rounded-xl border bg-card text-card-foreground shadow-sm",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function CardHeader({
19
+ className,
20
+ ...props
21
+ }: React.HTMLAttributes<HTMLDivElement>) {
22
+ return (
23
+ <div
24
+ data-slot="card-header"
25
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function CardTitle({
32
+ className,
33
+ ...props
34
+ }: React.HTMLAttributes<HTMLDivElement>) {
35
+ return (
36
+ <div
37
+ data-slot="card-title"
38
+ className={cn("font-semibold leading-none tracking-tight", className)}
39
+ {...props}
40
+ />
41
+ );
42
+ }
43
+
44
+ function CardDescription({
45
+ className,
46
+ ...props
47
+ }: React.HTMLAttributes<HTMLDivElement>) {
48
+ return (
49
+ <div
50
+ data-slot="card-description"
51
+ className={cn("text-sm text-muted-foreground", className)}
52
+ {...props}
53
+ />
54
+ );
55
+ }
56
+
57
+ function CardContent({
58
+ className,
59
+ ...props
60
+ }: React.HTMLAttributes<HTMLDivElement>) {
61
+ return (
62
+ <div
63
+ data-slot="card-content"
64
+ className={cn("p-6 pt-0", className)}
65
+ {...props}
66
+ />
67
+ );
68
+ }
69
+
70
+ function CardFooter({
71
+ className,
72
+ ...props
73
+ }: React.HTMLAttributes<HTMLDivElement>) {
74
+ return (
75
+ <div
76
+ data-slot="card-footer"
77
+ className={cn("flex items-center p-6 pt-0", className)}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ export {
84
+ Card,
85
+ CardHeader,
86
+ CardFooter,
87
+ CardTitle,
88
+ CardDescription,
89
+ CardContent,
90
+ };
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true
11
+ },
12
+ "aliases": {
13
+ "components": "@/components",
14
+ "utils": "@/lib/utils",
15
+ "ui": "@/components/ui",
16
+ "lib": "@/lib",
17
+ "hooks": "@/hooks"
18
+ },
19
+ "iconLibrary": "lucide"
20
+ }
@@ -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,10 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ // `cn` — the shadcn class merger. clsx resolves conditional/array class
5
+ // inputs; tailwind-merge then dedupes conflicting Tailwind utilities so
6
+ // the last one wins (e.g. `cn("px-2", "px-4")` → "px-4"). Every shadcn
7
+ // component routes its className through this.
8
+ export function cn(...inputs: ClassValue[]) {
9
+ return twMerge(clsx(inputs));
10
+ }
@@ -0,0 +1,33 @@
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
+ "@pylonsync/client": "^__PYLON_VERSION__",
16
+ "react": "^19.0.0",
17
+ "react-dom": "^19.0.0",
18
+ "tailwindcss": "^4.3.0",
19
+ "@tailwindcss/cli": "^4.3.0",
20
+ "tw-animate-css": "^1.2.0",
21
+ "class-variance-authority": "^0.7.1",
22
+ "clsx": "^2.1.1",
23
+ "tailwind-merge": "^2.5.0",
24
+ "lucide-react": "^0.460.0",
25
+ "@radix-ui/react-slot": "^1.1.0"
26
+ },
27
+ "devDependencies": {
28
+ "@pylonsync/cli": "^__PYLON_VERSION__",
29
+ "@types/react": "^19.0.0",
30
+ "@types/react-dom": "^19.0.0",
31
+ "typescript": "^5.6.0"
32
+ }
33
+ }
@@ -0,0 +1,18 @@
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
+ "baseUrl": ".",
13
+ "paths": {
14
+ "@/*": ["./*"]
15
+ }
16
+ },
17
+ "include": ["app.ts", "app/**/*", "components/**/*", "lib/**/*"]
18
+ }
@@ -1,8 +1,10 @@
1
1
  # __APP_NAME__
2
2
 
3
- A full-stack [Pylon](https://pylonsync.com) app — a server-rendered homepage,
4
- email/password auth, and a live client dashboard over a synced database, all
5
- served from one binary on one port. No Next.js, no separate API server.
3
+ A full-stack, multi-tenant SaaS starter on [Pylon](https://pylonsync.com),
4
+ branded as a fictional product called **Acme**: a server-rendered marketing
5
+ landing page, email/password auth, organizations with members + roles, and
6
+ tenant-scoped projects — all from one binary on one port. No Next.js, no
7
+ separate API server, no realtime sidecar.
6
8
 
7
9
  ## Develop
8
10
 
@@ -10,42 +12,55 @@ served from one binary on one port. No Next.js, no separate API server.
10
12
  __RUN_DEV__
11
13
  ```
12
14
 
13
- Open http://localhost:4321. Sign up, and your notes dashboard updates live
14
- (open a second tab to watch writes sync). Edit any file under `app/` and save —
15
- the page reloads instantly.
15
+ Open http://localhost:4321. You get the **Acme landing page**. Sign up, create
16
+ an organization, and you land in a **workspace** with tenant-scoped projects and
17
+ a members panel. Create a second org and switch between them — each org's data
18
+ is private to it. Edit any file under `app/` and save — the page reloads.
16
19
 
17
20
  ## Layout
18
21
 
19
22
  ```
20
- app.ts data model + manifest (entities, policies, auth, routes)
21
- app/page.tsx "/" — the server-rendered, auth-aware homepage
22
- app/login,signup/ email/password forms (POST /api/auth/password/*)
23
- app/dashboard/ "/dashboard" authed; server-gated, live notes + sign out
24
- app/auth-form.tsx shared client island for the login/signup forms
25
- app/layout.tsx root layout wrapping every page (auth-aware nav)
26
- app/globals.css Tailwind entrypoint (compiled by Pylon)
27
- functions/ server functions (query/mutation/action) — typed RPC
23
+ app.ts User + Org/OrgMember/OrgInvite + tenant-scoped Project
24
+ app/page.tsx "/" — the server-rendered Acme landing page (auth-aware)
25
+ app/layout.tsx marketing nav + footer (rebrand "Acme")
26
+ app/login,signup/ email/password (POST /api/auth/password/*)
27
+ app/dashboard/ "/dashboard" authed; org switcher + projects + members
28
+ app/dashboard/dashboard-client.tsx the workspace client island
29
+ app/globals.css Tailwind v4 + shadcn tokens (compiled by Pylon)
30
+ components/ui/ shadcn primitives (Button, Card)
28
31
  ```
29
32
 
30
- ## How auth works
33
+ ## How it works
31
34
 
32
- Email/password is built in. `/login` and `/signup` call
33
- `/api/auth/password/*`; on success the server sets an **HttpOnly session
34
- cookie** (no token in JS-readable storage). `/dashboard` reads `auth` during
35
- the server render and redirects anonymous visitors to `/login` a real 3xx
36
- before any HTML, so there's no flash and it works with JS off. The sync engine
37
- authenticates with the same cookie.
35
+ **The landing page** (`app/page.tsx`) is server-rendered React view source and
36
+ the copy + SEO `<head>` are in the HTML, so it's fully indexable. It reads the
37
+ session during the render, so the call-to-action is "Get started" for visitors
38
+ and "Open dashboard" once you're signed in no flash, no client fetch.
38
39
 
39
- ## Add a route
40
+ **Auth** is built in: `/login` + `/signup` POST to `/api/auth/password/*`, the
41
+ server sets an HttpOnly session cookie, and `/dashboard` redirects anonymous
42
+ visitors with a real 3xx before any HTML (works with JS off).
40
43
 
41
- Drop a file at `app/about/page.tsx` and visit `/about`. Pages receive
42
- `{ url, params, searchParams, auth, response, serverData }` from the SSR
43
- runtime — all typed via `PageProps` from `@pylonsync/react`.
44
+ **Multi-tenancy** is a framework primitive. Declaring `Org` / `OrgMember` /
45
+ `OrgInvite` lights up `/api/auth/orgs/*` + `/api/auth/select-org`, driven by
46
+ `<OrganizationSwitcher>` from `@pylonsync/client`. Your data lives in
47
+ tenant-scoped entities (`Project`), gated by policy:
44
48
 
45
- ## Add data
49
+ ```ts
50
+ allowRead: "auth.tenantId == data.orgId"
51
+ allowInsert: "auth.tenantId == data.orgId"
52
+ ```
53
+
54
+ So `db.useQuery("Project")` returns only your **active org's** projects — switch
55
+ orgs and the list changes, and a client literally cannot read or write another
56
+ tenant's rows. `db.useQuery` is live; `db.insert` is optimistic.
57
+
58
+ ## Make it yours
46
59
 
47
- Edit `app.ts`. Every `entity()` becomes a synced table with a REST +
48
- realtime API and a typed client no migrations, no resolvers.
60
+ - **Rebrand:** replace "Acme" in `app/page.tsx` + `app/layout.tsx`.
61
+ - **Add tenant data:** new `entity()` with an `orgId` + the same two policy
62
+ lines — a new tenant-scoped table, typed client and REST/realtime API included.
63
+ - **Add a route:** drop `app/about/page.tsx` and visit `/about`.
49
64
 
50
65
  ## Deploy
51
66