@pylonsync/create-pylon 0.9.1 → 0.10.0

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 (48) hide show
  1. package/package.json +1 -1
  2. package/templates/_root/AGENTS.md +11 -0
  3. package/templates/agency/AGENTS.md +11 -0
  4. package/templates/ai-chat/AGENTS.md +11 -0
  5. package/templates/ai-studio/AGENTS.md +11 -0
  6. package/templates/backend/mobile/apps/api/README.md +20 -1
  7. package/templates/backend/mobile/apps/api/app/(site)/layout.tsx +103 -0
  8. package/templates/backend/mobile/apps/api/app/(site)/not-found.tsx +23 -0
  9. package/templates/backend/mobile/apps/api/app/(site)/page.tsx +221 -0
  10. package/templates/backend/mobile/apps/api/app/(site)/privacy/page.tsx +131 -0
  11. package/templates/backend/mobile/apps/api/app/(site)/support/page.tsx +78 -0
  12. package/templates/backend/mobile/apps/api/app/(site)/terms/page.tsx +144 -0
  13. package/templates/backend/mobile/apps/api/app/error.tsx +27 -0
  14. package/templates/backend/mobile/apps/api/app/globals.css +87 -0
  15. package/templates/backend/mobile/apps/api/app/layout.tsx +27 -0
  16. package/templates/backend/mobile/apps/api/app/robots.ts +13 -0
  17. package/templates/backend/mobile/apps/api/app/sitemap.ts +16 -0
  18. package/templates/backend/mobile/apps/api/app.ts +6 -2
  19. package/templates/backend/mobile/apps/api/components/legal.tsx +64 -0
  20. package/templates/backend/mobile/apps/api/lib/site.ts +94 -0
  21. package/templates/backend/mobile/apps/api/package.json +8 -1
  22. package/templates/backend/mobile/apps/api/tsconfig.json +6 -2
  23. package/templates/barebones/AGENTS.md +11 -0
  24. package/templates/chat/AGENTS.md +11 -0
  25. package/templates/consumer/AGENTS.md +11 -0
  26. package/templates/creator/AGENTS.md +11 -0
  27. package/templates/crm/AGENTS.md +11 -0
  28. package/templates/directory/AGENTS.md +11 -0
  29. package/templates/expo/mobile/apps/expo/.env.example +13 -4
  30. package/templates/expo/mobile/apps/expo/README.md +9 -0
  31. package/templates/expo/mobile/apps/expo/STORE.md +9 -5
  32. package/templates/expo/mobile/apps/expo/app/(tabs)/settings.tsx +9 -6
  33. package/templates/expo/mobile/apps/expo/app/paywall.tsx +9 -13
  34. package/templates/expo/mobile/apps/expo/src/links.ts +29 -0
  35. package/templates/helpdesk/AGENTS.md +11 -0
  36. package/templates/inventory/AGENTS.md +11 -0
  37. package/templates/invoices/AGENTS.md +11 -0
  38. package/templates/local-service/AGENTS.md +11 -0
  39. package/templates/marketplace/AGENTS.md +11 -0
  40. package/templates/projects/AGENTS.md +11 -0
  41. package/templates/restaurant/AGENTS.md +11 -0
  42. package/templates/saas/AGENTS.md +11 -0
  43. package/templates/saas/app/dashboard/layout.tsx +9 -2
  44. package/templates/saas/app/dashboard/page.tsx +37 -14
  45. package/templates/saas/app/dashboard/settings/page.tsx +11 -4
  46. package/templates/shop/AGENTS.md +11 -0
  47. package/templates/todo/AGENTS.md +11 -0
  48. package/templates/waitlist/AGENTS.md +11 -0
@@ -0,0 +1,78 @@
1
+ import React from "react";
2
+ import { Link, type Metadata } from "@pylonsync/react";
3
+ import { site } from "@/lib/site";
4
+
5
+ export const metadata: Metadata = {
6
+ title: `Support — ${site.name}`,
7
+ description: `Get help with ${site.name}.`,
8
+ };
9
+
10
+ /**
11
+ * App Store Connect asks for a support URL, and Play requires a contact
12
+ * address on the listing. This page is both. The answers repeat what the app
13
+ * does so a reviewer can check a claim without installing anything.
14
+ */
15
+ export default function SupportPage() {
16
+ return (
17
+ <div className="mx-auto max-w-3xl px-6 py-16 sm:py-20">
18
+ <h1 className="text-3xl font-semibold tracking-[-0.02em]">Support</h1>
19
+ <p className="mt-4 text-[15px] leading-relaxed text-ink-muted">
20
+ {site.supportEmail ? (
21
+ <>
22
+ Email{" "}
23
+ <a
24
+ href={`mailto:${site.supportEmail}`}
25
+ className="text-brand underline underline-offset-2"
26
+ >
27
+ {site.supportEmail}
28
+ </a>{" "}
29
+ and tell us what happened, what you expected, and which device you
30
+ are on. That is usually enough to sort it out in one reply.
31
+ </>
32
+ ) : (
33
+ <>Add a support address in lib/site.ts and it will appear here.</>
34
+ )}
35
+ </p>
36
+
37
+ <h2 className="mt-12 text-xl font-semibold tracking-[-0.015em]">
38
+ Common questions
39
+ </h2>
40
+ <dl className="mt-6 divide-y divide-line border-y border-line">
41
+ {site.faq.map((item) => (
42
+ <div key={item.q} className="py-5">
43
+ <dt className="text-[15px] font-medium">{item.q}</dt>
44
+ <dd className="mt-2 text-[14px] leading-relaxed text-ink-muted">
45
+ {item.a}
46
+ </dd>
47
+ </div>
48
+ ))}
49
+ <div className="py-5">
50
+ <dt className="text-[15px] font-medium">
51
+ How do I delete my account?
52
+ </dt>
53
+ <dd className="mt-2 text-[14px] leading-relaxed text-ink-muted">
54
+ In the app, open Settings and choose Delete account. It removes the
55
+ account and its notes straight away. Cancel a subscription
56
+ separately in the App Store or Play Store.
57
+ </dd>
58
+ </div>
59
+ <div className="py-5">
60
+ <dt className="text-[15px] font-medium">
61
+ My notes are not showing on my other device.
62
+ </dt>
63
+ <dd className="mt-2 text-[14px] leading-relaxed text-ink-muted">
64
+ Check that both devices are signed in to the same account. Notes
65
+ written before you signed in move to the account the first time you
66
+ sign in on that device.
67
+ </dd>
68
+ </div>
69
+ </dl>
70
+
71
+ <p className="mt-10 text-[14px] text-ink-muted">
72
+ See also our <Link href="/privacy" className="text-brand underline underline-offset-2">privacy policy</Link>{" "}
73
+ and{" "}
74
+ <Link href="/terms" className="text-brand underline underline-offset-2">terms</Link>.
75
+ </p>
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,144 @@
1
+ import React from "react";
2
+ import { type Metadata } from "@pylonsync/react";
3
+ import { Company, LegalPage } from "@/components/legal";
4
+ import { site } from "@/lib/site";
5
+
6
+ export const metadata: Metadata = {
7
+ title: `Terms of Service — ${site.name}`,
8
+ description: `The agreement between you and ${site.name}.`,
9
+ };
10
+
11
+ /**
12
+ * The terms the app links to from the paywall and from Settings. Apple
13
+ * requires subscription terms to be visible next to the purchase, and this
14
+ * page is where the detail lives. Keep the billing section true to what your
15
+ * products actually do.
16
+ */
17
+ export default function TermsPage() {
18
+ return (
19
+ <LegalPage title="Terms of Service">
20
+ <p>
21
+ These terms are the agreement between you and <Company /> for the use
22
+ of {site.name}. Using the app means you accept them.
23
+ </p>
24
+
25
+ <h2>Who may use it</h2>
26
+ <p>
27
+ You need to be at least 13 years old. If you use {site.name} for an
28
+ organization, you confirm you may accept these terms for it.
29
+ </p>
30
+
31
+ <h2>Your account</h2>
32
+ <p>
33
+ You are responsible for what happens under your account, so keep
34
+ access to your email and devices secure. Tell us if you think someone
35
+ else has your account.
36
+ </p>
37
+
38
+ <h2>Free and paid</h2>
39
+ <p>
40
+ The free tier holds up to {site.pricing.freeLimit} notes. A
41
+ subscription lifts that limit and includes what we add later. We may
42
+ change what each tier includes, and we will not reduce what you have
43
+ already paid for during a period you have paid.
44
+ </p>
45
+
46
+ <h2>Billing</h2>
47
+ <ul>
48
+ <li>
49
+ Apple and Google sell and bill the subscription, not us. The price
50
+ you see in the app is the price they charge in your country.
51
+ </li>
52
+ <li>
53
+ A subscription renews automatically until you cancel it. Cancel at
54
+ least 24 hours before the period ends, in your App Store or Play
55
+ Store account settings.
56
+ </li>
57
+ <li>
58
+ Refunds follow the store&apos;s policy. We cannot issue a refund for
59
+ a purchase we did not process, so ask Apple or Google.
60
+ </li>
61
+ <li>
62
+ Deleting the app does not cancel a subscription. Cancel it in the
63
+ store.
64
+ </li>
65
+ </ul>
66
+
67
+ <h2>Your content</h2>
68
+ <p>
69
+ Your notes are yours. You give us only the permission we need to run
70
+ the service, which is to store your content, back it up, and send it to
71
+ the devices you sign in on. We claim no ownership of it and we do not
72
+ use it to train anything.
73
+ </p>
74
+ <p>
75
+ You are responsible for what you put in the app, and for having the
76
+ right to put it there.
77
+ </p>
78
+
79
+ <h2>Acceptable use</h2>
80
+ <p>Do not use {site.name} to:</p>
81
+ <ul>
82
+ <li>break the law, or store material that is illegal to hold;</li>
83
+ <li>
84
+ attack the service, work around its limits, or get at other
85
+ people&apos;s data;
86
+ </li>
87
+ <li>resell it as your own product.</li>
88
+ </ul>
89
+
90
+ <h2>Availability</h2>
91
+ <p>
92
+ We work to keep {site.name} running, but we do not promise it will be
93
+ available without interruption. We may change or stop features. If we
94
+ shut the service down, we will give reasonable notice and a way to
95
+ export your notes.
96
+ </p>
97
+
98
+ <h2>Ending it</h2>
99
+ <p>
100
+ You can stop at any time by deleting your account in Settings. We may
101
+ suspend or close an account that breaks these terms or puts the service
102
+ or other people at risk.
103
+ </p>
104
+
105
+ <h2>Disclaimer and liability</h2>
106
+ <p>
107
+ {site.name} is provided as it is, without warranties beyond those the
108
+ law requires. To the extent the law allows, <Company /> is not liable
109
+ for indirect or consequential loss, and our total liability is limited
110
+ to what you paid us in the 12 months before the claim.
111
+ </p>
112
+ <p>
113
+ Nothing here removes rights you have as a consumer that cannot be
114
+ removed by agreement.
115
+ </p>
116
+
117
+ <h2>Changes</h2>
118
+ <p>
119
+ We may update these terms. The date at the top shows the current
120
+ version, and we will tell you in the app about a significant change.
121
+ Continuing to use {site.name} after that means you accept the new
122
+ terms.
123
+ </p>
124
+
125
+ <h2>Governing law</h2>
126
+ <p>
127
+ These terms are governed by the laws of{" "}
128
+ {site.governingLaw || "[your jurisdiction]"}, without regard to
129
+ conflict of law rules.
130
+ </p>
131
+
132
+ <h2>Contact</h2>
133
+ <p>
134
+ <Company />
135
+ {site.address ? <>, {site.address}</> : null}
136
+ {site.supportEmail ? (
137
+ <>
138
+ . Email <a href={`mailto:${site.supportEmail}`}>{site.supportEmail}</a>.
139
+ </>
140
+ ) : null}
141
+ </p>
142
+ </LegalPage>
143
+ );
144
+ }
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import { type ErrorBoundaryProps } from "@pylonsync/react";
3
+
4
+ // The error boundary sits at the root, outside the route group, so it still
5
+ // renders if a layout itself throws. It is hydrated, so `reset()` works.
6
+ export default function ErrorBoundary({ error, reset }: ErrorBoundaryProps) {
7
+ return (
8
+ <div className="mx-auto max-w-3xl px-6 py-24 text-center">
9
+ <h1 className="text-3xl font-semibold tracking-[-0.02em]">
10
+ Something went wrong
11
+ </h1>
12
+ <p className="mt-3 text-[15px] text-ink-muted">{error.message}</p>
13
+ {error.digest ? (
14
+ <p className="mt-1 text-[12px] text-ink-muted">
15
+ Reference: <code className="font-mono">{error.digest}</code>
16
+ </p>
17
+ ) : null}
18
+ <button
19
+ type="button"
20
+ onClick={reset}
21
+ className="mt-8 rounded-xl bg-ink px-5 py-3 text-[14px] font-medium text-surface transition-opacity hover:opacity-90"
22
+ >
23
+ Try again
24
+ </button>
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,87 @@
1
+ @import "tailwindcss";
2
+
3
+ /* Tailwind v4 scans these globs for class names. Add a line if you put
4
+ markup somewhere else. */
5
+ @source "../app/**/*.{tsx,ts,jsx,js}";
6
+ @source "../components/**/*.{tsx,ts,jsx,js}";
7
+ @source "../lib/**/*.{tsx,ts,jsx,js}";
8
+
9
+ /* The whole site re-themes from these. `--brand` is the accent on buttons
10
+ and links; the ink/surface pair carries everything else. */
11
+ :root {
12
+ --brand: #4f46e5;
13
+ --brand-ink: #ffffff;
14
+ --brand-soft: #eef2ff;
15
+ --ink: #18181b;
16
+ --ink-muted: #52525b;
17
+ --surface: #ffffff;
18
+ --surface-soft: #fafafa;
19
+ --line: #e4e4e7;
20
+ }
21
+
22
+ @media (prefers-color-scheme: dark) {
23
+ :root {
24
+ --brand: #818cf8;
25
+ --brand-ink: #1e1b4b;
26
+ --brand-soft: #1e1b4b;
27
+ --ink: #fafafa;
28
+ --ink-muted: #a1a1aa;
29
+ --surface: #09090b;
30
+ --surface-soft: #131316;
31
+ --line: #27272a;
32
+ }
33
+ }
34
+
35
+ @theme inline {
36
+ --color-brand: var(--brand);
37
+ --color-brand-ink: var(--brand-ink);
38
+ --color-brand-soft: var(--brand-soft);
39
+ --color-ink: var(--ink);
40
+ --color-ink-muted: var(--ink-muted);
41
+ --color-surface: var(--surface);
42
+ --color-surface-soft: var(--surface-soft);
43
+ --color-line: var(--line);
44
+ --font-sans:
45
+ ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
46
+ "Helvetica Neue", Arial, sans-serif;
47
+ }
48
+
49
+ html {
50
+ -webkit-text-size-adjust: 100%;
51
+ }
52
+
53
+ body {
54
+ font-family: var(--font-sans);
55
+ -webkit-font-smoothing: antialiased;
56
+ text-rendering: optimizeLegibility;
57
+ }
58
+
59
+ /* Long-form legal copy. The legal pages are plain prose, so style the
60
+ elements once here instead of putting a class on every paragraph. */
61
+ .prose-legal > * + * {
62
+ margin-top: 1rem;
63
+ }
64
+ .prose-legal h2 {
65
+ margin-top: 2.5rem;
66
+ font-size: 1.125rem;
67
+ font-weight: 600;
68
+ letter-spacing: -0.01em;
69
+ color: var(--ink);
70
+ }
71
+ .prose-legal p,
72
+ .prose-legal li {
73
+ color: var(--ink-muted);
74
+ line-height: 1.7;
75
+ }
76
+ .prose-legal ul {
77
+ margin-left: 1.125rem;
78
+ list-style: disc;
79
+ }
80
+ .prose-legal li + li {
81
+ margin-top: 0.375rem;
82
+ }
83
+ .prose-legal a {
84
+ color: var(--brand);
85
+ text-decoration: underline;
86
+ text-underline-offset: 2px;
87
+ }
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+
3
+ interface LayoutProps {
4
+ children: React.ReactNode;
5
+ }
6
+
7
+ /**
8
+ * The document shell. Chrome lives in the section layout
9
+ * (`app/(site)/layout.tsx`), which is what wraps the pages with the header
10
+ * and footer. Tailwind's compiled stylesheet is injected into <head> by the
11
+ * runtime.
12
+ *
13
+ * No <title> here on purpose: each page exports its own `metadata`, and a
14
+ * title in the layout would render first and win.
15
+ */
16
+ export default function RootLayout({ children }: LayoutProps) {
17
+ return (
18
+ <html lang="en">
19
+ <head>
20
+ <meta charSet="utf-8" />
21
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
22
+ <meta name="color-scheme" content="light dark" />
23
+ </head>
24
+ <body className="min-h-screen bg-surface text-ink">{children}</body>
25
+ </html>
26
+ );
27
+ }
@@ -0,0 +1,13 @@
1
+ import type { Robots } from "@pylonsync/react";
2
+ import { site } from "@/lib/site";
3
+
4
+ // app/robots.ts → served at /robots.txt.
5
+ const SITE = site.url || process.env.SITE_URL || "http://localhost:4321";
6
+
7
+ export default function robots(): Robots {
8
+ return {
9
+ // The API is not for crawlers; the marketing and legal pages are.
10
+ rules: { userAgent: "*", allow: "/", disallow: ["/api/"] },
11
+ sitemap: `${SITE}/sitemap.xml`,
12
+ };
13
+ }
@@ -0,0 +1,16 @@
1
+ import type { Sitemap } from "@pylonsync/react";
2
+ import { site } from "@/lib/site";
3
+
4
+ // app/sitemap.ts → served at /sitemap.xml. This is a server-only route
5
+ // module, never bundled for the browser, so reading process.env here is
6
+ // safe (page components cannot).
7
+ const SITE = site.url || process.env.SITE_URL || "http://localhost:4321";
8
+
9
+ export default function sitemap(): Sitemap {
10
+ return [
11
+ { url: `${SITE}/`, changeFrequency: "weekly", priority: 1 },
12
+ { url: `${SITE}/support`, changeFrequency: "monthly", priority: 0.5 },
13
+ { url: `${SITE}/privacy`, changeFrequency: "yearly", priority: 0.3 },
14
+ { url: `${SITE}/terms`, changeFrequency: "yearly", priority: 0.3 },
15
+ ];
16
+ }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  auth,
3
3
  buildManifest,
4
+ discoverAppRoutes,
4
5
  discoverFunctions,
5
6
  entity,
6
7
  field,
@@ -73,8 +74,11 @@ const manifest = buildManifest({
73
74
  // register each twice.
74
75
  actions: fns.actions,
75
76
  policies: [userPolicy, notePolicy, ...purchases.manifest.policies],
76
- // API only: the Expo app is the frontend.
77
- routes: [],
77
+ // The public website: landing page, support, privacy policy, and terms,
78
+ // under `app/`. One Pylon server answers the Expo app's API calls AND
79
+ // serves these pages, so the legal URLs both stores demand are live on the
80
+ // same host as soon as you deploy. Content lives in `lib/site.ts`.
81
+ routes: await discoverAppRoutes(),
78
82
  // Email/password + magic codes are on by default. Native sign-in needs
79
83
  // the app's ids on the server: PYLON_APPLE_NATIVE_CLIENT_IDS (the iOS
80
84
  // bundle id) and PYLON_GOOGLE_NATIVE_CLIENT_IDS (the iOS + Android OAuth
@@ -0,0 +1,64 @@
1
+ import React from "react";
2
+ import { site } from "@/lib/site";
3
+
4
+ /**
5
+ * The frame both legal pages share: title, effective date, and the notice
6
+ * that this text is a starting point rather than reviewed legal advice.
7
+ *
8
+ * The notice is shown until `lib/site.ts` has your company details, on the
9
+ * same principle as the banner in the site layout: unfinished legal text
10
+ * should say so.
11
+ */
12
+ export function LegalPage({
13
+ title,
14
+ children,
15
+ }: {
16
+ title: string;
17
+ children: React.ReactNode;
18
+ }) {
19
+ const unfinished = !site.company || !site.supportEmail || !site.address;
20
+ return (
21
+ <article className="mx-auto max-w-3xl px-6 py-16 sm:py-20">
22
+ <h1 className="text-3xl font-semibold tracking-[-0.02em]">{title}</h1>
23
+ <p className="mt-3 text-[13px] text-ink-muted">
24
+ Effective {formatDate(site.legalEffective)}
25
+ {site.company ? ` · ${site.company}` : ""}
26
+ </p>
27
+ {unfinished ? (
28
+ <p className="mt-8 rounded-xl border border-amber-300 bg-amber-50 px-4 py-3 text-[13px] leading-relaxed text-amber-900">
29
+ Draft. This text describes what the app in this template actually
30
+ does, which makes it a useful starting point, but it has not been
31
+ reviewed by a lawyer and it does not know your business. Fill in
32
+ <code className="mx-1 font-mono">lib/site.ts</code>, read every
33
+ section, and have counsel check it before you submit to a store.
34
+ </p>
35
+ ) : null}
36
+ <div className="prose-legal mt-10">{children}</div>
37
+ <p className="mt-12 border-t border-line pt-6 text-[13px] text-ink-muted">
38
+ Questions about this page?{" "}
39
+ {site.supportEmail ? (
40
+ <a href={`mailto:${site.supportEmail}`}>{site.supportEmail}</a>
41
+ ) : (
42
+ "Add a support address in lib/site.ts."
43
+ )}
44
+ </p>
45
+ </article>
46
+ );
47
+ }
48
+
49
+ /** The legal entity, or a visible placeholder while the config is blank. */
50
+ export function Company() {
51
+ return <>{site.company || "[your company]"}</>;
52
+ }
53
+
54
+ function formatDate(iso: string): string {
55
+ // Build from the parts. `new Date("2026-01-01")` parses as UTC midnight and
56
+ // renders as the day before in any negative-UTC timezone.
57
+ const [y, m, d] = iso.split("-").map(Number);
58
+ if (!y || !m || !d) return iso;
59
+ return new Date(y, m - 1, d).toLocaleDateString("en-US", {
60
+ year: "numeric",
61
+ month: "long",
62
+ day: "numeric",
63
+ });
64
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Everything the marketing and legal pages read. This is the one file to
3
+ * edit when you rename the app, add store links, or fill in your company
4
+ * details.
5
+ *
6
+ * It is a plain module, not env vars, on purpose: SSR pages are bundled for
7
+ * hydration and the bundler rewrites `process.env.*`, so page code cannot
8
+ * read arbitrary env at render time. Server-only route modules
9
+ * (app/sitemap.ts, app/robots.ts) can, and do.
10
+ */
11
+
12
+ export const site = {
13
+ /** Shown in the header, the page titles, and the legal pages. */
14
+ name: "__APP_NAME__",
15
+ /** The hero headline. One sentence, what the app does for the person. */
16
+ tagline: "Your notes, on every device, the moment you write them",
17
+ /** The line under the headline, and the meta description. */
18
+ description:
19
+ "__APP_NAME__ keeps your notes in sync across your phone, tablet, and anything else you sign in on. It works offline and catches up when you are back.",
20
+
21
+ /**
22
+ * The public URL of this site once deployed, with no trailing slash.
23
+ * `pylon deploy` prints the host. Used for canonical links and the
24
+ * sitemap, and by the mobile app when it opens the legal pages.
25
+ */
26
+ url: "",
27
+
28
+ /** The legal entity that publishes the app. Appears on both legal pages. */
29
+ company: "",
30
+ /** Where support email and legal notices go. */
31
+ supportEmail: "",
32
+ /** Postal address. Apple asks for one on the privacy policy. */
33
+ address: "",
34
+ /** The law that governs the terms, e.g. "the State of Texas, USA". */
35
+ governingLaw: "",
36
+ /** Date the current legal text took effect, as YYYY-MM-DD. */
37
+ legalEffective: "2026-01-01",
38
+
39
+ /** Store links. Empty renders a "coming soon" state instead of a dead link. */
40
+ appStoreUrl: "",
41
+ playStoreUrl: "",
42
+
43
+ /** The three or four things worth saying on the landing page. */
44
+ features: [
45
+ {
46
+ title: "Write now, sync later",
47
+ body: "Notes save on your device first, so the app never waits on a network. They reach your other devices as soon as there is one.",
48
+ },
49
+ {
50
+ title: "Start without an account",
51
+ body: "Open the app and use it. Sign in when you want your notes on a second device, and everything you already wrote comes with you.",
52
+ },
53
+ {
54
+ title: "Yours to delete",
55
+ body: "Delete your account in Settings and your notes go with it. No support ticket, no waiting.",
56
+ },
57
+ ],
58
+
59
+ /** Free tier and subscription, kept honest with what the app enforces. */
60
+ pricing: {
61
+ freeLimit: 10,
62
+ proBlurb: "Unlimited notes and everything shipped next.",
63
+ },
64
+
65
+ faq: [
66
+ {
67
+ q: "Do I need an account?",
68
+ a: "No. The app works straight away. Signing in keeps your notes if you switch phones.",
69
+ },
70
+ {
71
+ q: "How do I cancel a subscription?",
72
+ a: "Subscriptions are billed by Apple or Google, so cancel from your App Store or Play Store account. Access lasts until the end of the period you paid for.",
73
+ },
74
+ {
75
+ q: "What happens to my notes if I delete my account?",
76
+ a: "They are deleted with it. Settings has the button, and it takes effect immediately.",
77
+ },
78
+ ],
79
+ } as const;
80
+
81
+ /**
82
+ * Config the site cannot honestly render without. The header shows a notice
83
+ * while any of these are blank, so an unfinished site cannot quietly go live
84
+ * with placeholder legal text.
85
+ */
86
+ export function missingSiteConfig(): string[] {
87
+ const missing: string[] = [];
88
+ if (!site.url) missing.push("url");
89
+ if (!site.company) missing.push("company");
90
+ if (!site.supportEmail) missing.push("supportEmail");
91
+ if (!site.address) missing.push("address");
92
+ if (!site.governingLaw) missing.push("governingLaw");
93
+ return missing;
94
+ }
@@ -13,11 +13,18 @@
13
13
  "dependencies": {
14
14
  "@pylonsync/sdk": "^__PYLON_VERSION__",
15
15
  "@pylonsync/functions": "^__PYLON_VERSION__",
16
- "@pylonsync/revenuecat": "^__PYLON_VERSION__"
16
+ "@pylonsync/react": "^__PYLON_VERSION__",
17
+ "@pylonsync/revenuecat": "^__PYLON_VERSION__",
18
+ "react": "^19.0.0",
19
+ "react-dom": "^19.0.0",
20
+ "tailwindcss": "^4.3.0",
21
+ "@tailwindcss/cli": "^4.3.0"
17
22
  },
18
23
  "devDependencies": {
19
24
  "@pylonsync/cli": "^__PYLON_VERSION__",
20
25
  "@types/node": "^22.0.0",
26
+ "@types/react": "^19.0.0",
27
+ "@types/react-dom": "^19.0.0",
21
28
  "typescript": "^5.6.0"
22
29
  }
23
30
  }
@@ -3,11 +3,15 @@
3
3
  "target": "ES2022",
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "bundler",
6
+ "jsx": "react",
6
7
  "strict": true,
7
8
  "esModuleInterop": true,
8
9
  "skipLibCheck": true,
9
10
  "noEmit": true,
10
- "types": ["node"]
11
+ "lib": ["ES2022", "DOM"],
12
+ "types": ["react", "react-dom", "node"],
13
+ "baseUrl": ".",
14
+ "paths": { "@/*": ["./*"] }
11
15
  },
12
- "include": ["app.ts", "functions", "lib"]
16
+ "include": ["app.ts", "app/**/*", "components/**/*", "functions/**/*", "lib/**/*"]
13
17
  }
@@ -48,6 +48,17 @@ this pass before the user has to ask.
48
48
  - **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
49
49
  - **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
50
50
  - **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
51
+ - **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
52
+
53
+ ```tsx
54
+ const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
55
+ const projectsPromise = serverData.list<Project>("Project");
56
+ const org = use(orgPromise);
57
+ const projects = use(projectsPromise);
58
+ ```
59
+
60
+ Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
61
+
51
62
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
52
63
  - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
53
64
  - **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
@@ -48,6 +48,17 @@ this pass before the user has to ask.
48
48
  - **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
49
49
  - **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
50
50
  - **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
51
+ - **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
52
+
53
+ ```tsx
54
+ const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
55
+ const projectsPromise = serverData.list<Project>("Project");
56
+ const org = use(orgPromise);
57
+ const projects = use(projectsPromise);
58
+ ```
59
+
60
+ Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
61
+
51
62
  - **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
52
63
  - **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
53
64
  - **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.