@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.
- package/bin/create-pylon.js +18 -10
- package/package.json +1 -1
- package/templates/b2b/AGENTS.md +61 -0
- package/templates/b2b/README.md +62 -0
- package/templates/b2b/app/auth-form.tsx +142 -0
- package/templates/b2b/app/dashboard/dashboard-client.tsx +192 -0
- package/templates/b2b/app/dashboard/page.tsx +63 -0
- package/templates/b2b/app/error.tsx +43 -0
- package/templates/b2b/app/globals.css +139 -0
- package/templates/b2b/app/layout.tsx +71 -0
- package/templates/b2b/app/login/page.tsx +47 -0
- package/templates/b2b/app/not-found.tsx +29 -0
- package/templates/b2b/app/page.tsx +114 -0
- package/templates/b2b/app/robots.ts +12 -0
- package/templates/b2b/app/signup/page.tsx +44 -0
- package/templates/b2b/app/sitemap.ts +27 -0
- package/templates/b2b/app.ts +179 -0
- package/templates/b2b/components/ui/button.tsx +56 -0
- package/templates/b2b/components/ui/card.tsx +90 -0
- package/templates/b2b/components.json +20 -0
- package/templates/b2b/functions/_keep.ts +13 -0
- package/templates/b2b/gitignore +10 -0
- package/templates/b2b/lib/utils.ts +10 -0
- package/templates/b2b/package.json +33 -0
- package/templates/b2b/tsconfig.json +18 -0
- package/templates/barebones/AGENTS.md +61 -0
- package/templates/barebones/README.md +45 -0
- package/templates/barebones/app/error.tsx +43 -0
- package/templates/barebones/app/globals.css +139 -0
- package/templates/barebones/app/items-client.tsx +96 -0
- package/templates/barebones/app/layout.tsx +27 -0
- package/templates/barebones/app/not-found.tsx +29 -0
- package/templates/barebones/app/page.tsx +28 -0
- package/templates/barebones/app/robots.ts +12 -0
- package/templates/barebones/app/sitemap.ts +27 -0
- package/templates/barebones/app.ts +55 -0
- package/templates/barebones/components/ui/button.tsx +56 -0
- package/templates/barebones/components/ui/card.tsx +90 -0
- package/templates/barebones/components.json +20 -0
- package/templates/barebones/functions/_keep.ts +13 -0
- package/templates/barebones/gitignore +10 -0
- package/templates/barebones/lib/utils.ts +10 -0
- package/templates/barebones/package.json +33 -0
- package/templates/barebones/tsconfig.json +18 -0
- package/templates/chat/AGENTS.md +61 -0
- package/templates/chat/README.md +51 -0
- package/templates/chat/app/chat-client.tsx +113 -0
- package/templates/chat/app/error.tsx +43 -0
- package/templates/chat/app/globals.css +139 -0
- package/templates/chat/app/layout.tsx +25 -0
- package/templates/chat/app/not-found.tsx +29 -0
- package/templates/chat/app/page.tsx +26 -0
- package/templates/chat/app/robots.ts +12 -0
- package/templates/chat/app/sitemap.ts +27 -0
- package/templates/chat/app.ts +59 -0
- package/templates/chat/components/ui/button.tsx +56 -0
- package/templates/chat/components/ui/card.tsx +90 -0
- package/templates/chat/components.json +20 -0
- package/templates/chat/functions/_keep.ts +13 -0
- package/templates/chat/gitignore +10 -0
- package/templates/chat/lib/utils.ts +10 -0
- package/templates/chat/package.json +33 -0
- package/templates/chat/tsconfig.json +18 -0
- package/templates/consumer/AGENTS.md +61 -0
- package/templates/consumer/README.md +52 -0
- package/templates/consumer/app/error.tsx +43 -0
- package/templates/consumer/app/feed-client.tsx +154 -0
- package/templates/consumer/app/globals.css +139 -0
- package/templates/consumer/app/layout.tsx +27 -0
- package/templates/consumer/app/not-found.tsx +29 -0
- package/templates/consumer/app/page.tsx +27 -0
- package/templates/consumer/app/robots.ts +12 -0
- package/templates/consumer/app/sitemap.ts +27 -0
- package/templates/consumer/app.ts +89 -0
- package/templates/consumer/components/ui/button.tsx +56 -0
- package/templates/consumer/components/ui/card.tsx +90 -0
- package/templates/consumer/components.json +20 -0
- package/templates/consumer/functions/_keep.ts +13 -0
- package/templates/consumer/gitignore +10 -0
- package/templates/consumer/lib/utils.ts +10 -0
- package/templates/consumer/package.json +33 -0
- package/templates/consumer/tsconfig.json +18 -0
- package/templates/ssr/README.md +43 -28
- package/templates/ssr/app/dashboard/dashboard-client.tsx +154 -78
- package/templates/ssr/app/dashboard/page.tsx +16 -60
- package/templates/ssr/app/layout.tsx +46 -39
- package/templates/ssr/app/login/page.tsx +1 -1
- package/templates/ssr/app/page.tsx +182 -84
- package/templates/ssr/app/signup/page.tsx +1 -1
- 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
|
+
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
|
+
}
|
package/templates/ssr/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# __APP_NAME__
|
|
2
2
|
|
|
3
|
-
A full-stack [Pylon](https://pylonsync.com)
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
21
|
-
app/page.tsx
|
|
22
|
-
app/
|
|
23
|
-
app/
|
|
24
|
-
app/
|
|
25
|
-
app/
|
|
26
|
-
app/globals.css
|
|
27
|
-
|
|
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
|
|
33
|
+
## How it works
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
`
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|