create-ej-app 25.0.0 → 27.0.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.
- package/README.md +125 -226
- package/cli.ts +83 -28
- package/package.json +2 -2
- package/template/api/.vscode/settings.json +1 -1
- package/template/api/{.github/copilot-instructions.md → AGENTS.md} +0 -2
- package/template/api/CLAUDE.md +1 -0
- package/template/api/package.json +10 -10
- package/template/nextjs/.dockerignore +1 -0
- package/template/nextjs/AGENTS.md +260 -0
- package/template/nextjs/CLAUDE.md +1 -0
- package/template/nextjs/README.md +109 -0
- package/template/nextjs/bunfig.toml +3 -0
- package/template/nextjs/eslint.config.mjs +6 -5
- package/template/nextjs/gitignore +6 -0
- package/template/nextjs/next.config.ts +8 -1
- package/template/nextjs/package.json +47 -40
- package/template/nextjs/public/logo-dark.svg +4 -0
- package/template/nextjs/public/logo.svg +4 -0
- package/template/nextjs/src/app/(admin)/actions/users.ts +183 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
- package/template/nextjs/src/app/(admin)/admin/layout.tsx +53 -0
- package/template/nextjs/src/app/(admin)/admin/page.tsx +11 -2
- package/template/nextjs/src/app/(admin)/admin/settings/layout.tsx +24 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/page.tsx +377 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/validation.ts +23 -0
- package/template/nextjs/src/app/(app)/app/layout.tsx +44 -0
- package/template/nextjs/src/app/(app)/app/page.tsx +14 -2
- package/template/nextjs/src/app/(auth)/(account)/layout.tsx +64 -0
- package/template/nextjs/src/app/(auth)/(account)/profile/page.tsx +16 -0
- package/template/nextjs/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
- package/template/nextjs/src/app/(auth)/login/email-login-form.tsx +56 -29
- package/template/nextjs/src/app/(auth)/login/page.tsx +45 -12
- package/template/nextjs/src/app/(auth)/login/social-login-buttons.tsx +85 -17
- package/template/nextjs/src/app/(marketing)/layout.tsx +35 -4
- package/template/nextjs/src/app/(marketing)/legal.tsx +56 -0
- package/template/nextjs/src/app/(marketing)/page.tsx +20 -2
- package/template/nextjs/src/app/(marketing)/privacy/page.tsx +43 -2
- package/template/nextjs/src/app/(marketing)/terms/page.tsx +33 -2
- package/template/nextjs/src/app/error.tsx +37 -0
- package/template/nextjs/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
- package/template/nextjs/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
- package/template/nextjs/src/app/fonts/index.ts +60 -0
- package/template/nextjs/src/app/global-error.tsx +38 -0
- package/template/nextjs/src/app/globals.css +29 -0
- package/template/nextjs/src/app/layout.tsx +45 -18
- package/template/nextjs/src/app/not-found.tsx +25 -3
- package/template/nextjs/src/app/robots.ts +29 -0
- package/template/nextjs/src/app/sitemap.ts +21 -0
- package/template/nextjs/src/components/confirm-dialog.tsx +79 -0
- package/template/nextjs/src/components/dashboard-header.tsx +70 -0
- package/template/nextjs/src/components/data-table/enum.ts +5 -0
- package/template/nextjs/src/components/data-table/features.ts +49 -0
- package/template/nextjs/src/components/data-table/index.tsx +961 -0
- package/template/nextjs/src/components/data-table/types.ts +76 -0
- package/template/nextjs/src/components/error.tsx +71 -0
- package/template/nextjs/src/components/loader.tsx +18 -2
- package/template/nextjs/src/components/logo.tsx +49 -0
- package/template/nextjs/src/components/navigation-search.tsx +140 -0
- package/template/nextjs/src/components/navigation-sidebar.tsx +241 -0
- package/template/nextjs/src/components/page-heading.tsx +38 -0
- package/template/nextjs/src/components/react-query-provider.tsx +41 -2
- package/template/nextjs/src/components/route-error.tsx +55 -0
- package/template/nextjs/src/components/screen-size.tsx +36 -0
- package/template/nextjs/src/components/site-footer.tsx +20 -4
- package/template/nextjs/src/components/theme-provider.tsx +48 -0
- package/template/nextjs/src/components/theme-toggle.tsx +58 -5
- package/template/nextjs/src/components/ui/alert-dialog.tsx +199 -0
- package/template/nextjs/src/components/ui/avatar.tsx +112 -0
- package/template/nextjs/src/components/ui/badge.tsx +54 -0
- package/template/nextjs/src/components/ui/button.tsx +3 -1
- package/template/nextjs/src/components/ui/card.tsx +4 -4
- package/template/nextjs/src/components/ui/collapsible.tsx +33 -0
- package/template/nextjs/src/components/ui/command.tsx +192 -0
- package/template/nextjs/src/components/ui/dialog.tsx +164 -0
- package/template/nextjs/src/components/ui/dropdown-menu.tsx +273 -0
- package/template/nextjs/src/components/ui/input-group.tsx +156 -0
- package/template/nextjs/src/components/ui/select.tsx +195 -0
- package/template/nextjs/src/components/ui/sheet.tsx +143 -0
- package/template/nextjs/src/components/ui/sidebar.tsx +702 -0
- package/template/nextjs/src/components/ui/skeleton.tsx +13 -0
- package/template/nextjs/src/components/ui/table.tsx +116 -0
- package/template/nextjs/src/components/ui/textarea.tsx +18 -0
- package/template/nextjs/src/components/ui/tooltip.tsx +57 -0
- package/template/nextjs/src/components/user-menu.tsx +206 -0
- package/template/nextjs/src/db/common.ts +10 -0
- package/template/nextjs/src/db/index.ts +16 -6
- package/template/nextjs/src/db/relations.ts +21 -0
- package/template/nextjs/src/db/schema.ts +85 -46
- package/template/nextjs/src/emails/components/email-layout.tsx +27 -11
- package/template/nextjs/src/emails/magic-link.tsx +15 -13
- package/template/nextjs/src/emails/test.tsx +11 -9
- package/template/nextjs/src/emails/welcome.tsx +12 -9
- package/template/nextjs/src/env.ts +1 -1
- package/template/nextjs/src/hooks/use-mobile.ts +14 -12
- package/template/nextjs/src/instrumentation.ts +20 -2
- package/template/nextjs/src/lib/actions.ts +55 -0
- package/template/nextjs/src/lib/admin-navigation.ts +43 -0
- package/template/nextjs/src/lib/auth-client.ts +20 -9
- package/template/nextjs/src/lib/auth-permissions.ts +61 -0
- package/template/nextjs/src/lib/auth.ts +33 -3
- package/template/nextjs/src/lib/callback-url.ts +28 -0
- package/template/nextjs/src/lib/constants.ts +21 -0
- package/template/nextjs/src/lib/delay.ts +10 -0
- package/template/nextjs/src/lib/email-service.ts +21 -0
- package/template/nextjs/src/lib/errors.ts +74 -0
- package/template/nextjs/src/lib/format-currency.ts +22 -24
- package/template/nextjs/src/lib/format-date.ts +69 -6
- package/template/nextjs/src/lib/nodemailer.ts +27 -3
- package/template/nextjs/src/lib/otel.ts +92 -2
- package/template/nextjs/src/lib/safe-action-client.ts +60 -9
- package/template/nextjs/src/lib/safe-action.ts +152 -16
- package/template/nextjs/src/lib/search.ts +16 -0
- package/template/nextjs/src/lib/seo.ts +7 -0
- package/template/nextjs/src/lib/siteConfig.ts +17 -1
- package/template/nextjs/src/lib/toast-message.ts +87 -1
- package/template/nextjs/src/lib/validations.ts +30 -0
- package/template/nextjs/src/proxy.ts +48 -0
- package/template/nextjs/src/scripts/reset-db.ts +22 -0
- package/template/nextjs/src/scripts/seed.ts +59 -8
- package/template/nextjs/src/scripts/test-mail.ts +28 -20
- package/template/nextjs/tests/admin/users.test.ts +64 -0
- package/template/nextjs/tests/db/fk-indexes.test.ts +62 -0
- package/template/nextjs/tests/db/seed.test.ts +49 -0
- package/template/nextjs/tests/helpers/action.ts +71 -0
- package/template/nextjs/tests/helpers/fixtures.ts +78 -0
- package/template/nextjs/tests/helpers/index.ts +2 -0
- package/template/nextjs/tests/lib/callback-url.test.ts +48 -0
- package/template/nextjs/tests/lib/client-server-boundary.test.ts +57 -0
- package/template/nextjs/tests/lib/errors.test.ts +10 -0
- package/template/nextjs/tests/lib/search.test.ts +23 -0
- package/template/nextjs/tests/setup/pglite-db.ts +63 -0
- package/template/nextjs/tests/setup/preload.ts +95 -0
- package/template/nextjs/tsconfig.json +1 -0
- package/template/saas-starter/.dockerignore +8 -0
- package/template/saas-starter/.env.example +25 -0
- package/template/saas-starter/.vscode/extensions.json +11 -0
- package/template/saas-starter/.vscode/settings.json +3 -0
- package/template/saas-starter/AGENTS.md +302 -0
- package/template/saas-starter/CLAUDE.md +1 -0
- package/template/saas-starter/Dockerfile +15 -0
- package/template/saas-starter/README.md +125 -0
- package/template/saas-starter/bunfig.toml +3 -0
- package/template/saas-starter/components.json +25 -0
- package/template/saas-starter/drizzle.config.ts +10 -0
- package/template/saas-starter/eslint.config.mjs +24 -0
- package/template/saas-starter/gitignore +14 -0
- package/template/saas-starter/next.config.ts +32 -0
- package/template/saas-starter/package.json +97 -0
- package/template/saas-starter/postcss.config.mjs +7 -0
- package/template/saas-starter/prettier.config.mjs +11 -0
- package/template/saas-starter/public/logo-dark.svg +4 -0
- package/template/saas-starter/public/logo.svg +4 -0
- package/template/saas-starter/public/placeholder.svg +1 -0
- package/template/saas-starter/src/app/(admin)/actions/contact-leads.ts +51 -0
- package/template/saas-starter/src/app/(admin)/actions/feedbacks.ts +91 -0
- package/template/saas-starter/src/app/(admin)/actions/users.ts +183 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
- package/template/saas-starter/src/app/(admin)/admin/contact-leads/page.tsx +123 -0
- package/template/saas-starter/src/app/(admin)/admin/feedbacks/page.tsx +161 -0
- package/template/saas-starter/src/app/(admin)/admin/layout.tsx +53 -0
- package/template/saas-starter/src/app/(admin)/admin/page.tsx +60 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/layout.tsx +24 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +377 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/validation.ts +23 -0
- package/template/saas-starter/src/app/(admin)/admin/support-tickets/[ticketId]/page.tsx +17 -0
- package/template/saas-starter/src/app/(admin)/admin/support-tickets/page.tsx +5 -0
- package/template/saas-starter/src/app/(app)/actions/billing.ts +216 -0
- package/template/saas-starter/src/app/(app)/actions/feedbacks.ts +28 -0
- package/template/saas-starter/src/app/(app)/actions/feedbacks.validation.ts +12 -0
- package/template/saas-starter/src/app/(app)/actions/members.ts +189 -0
- package/template/saas-starter/src/app/(app)/actions/organizations.ts +230 -0
- package/template/saas-starter/src/app/(app)/actions/organizations.validation.ts +17 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +17 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +31 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +92 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +357 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +130 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +222 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +115 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/layout.tsx +24 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +168 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +604 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +33 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +55 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/[ticketId]/page.tsx +25 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/page.tsx +25 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +31 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +26 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +30 -0
- package/template/saas-starter/src/app/(app)/app/_components/feedback-dialog.tsx +108 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +37 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +200 -0
- package/template/saas-starter/src/app/(app)/app/layout.tsx +64 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +117 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +40 -0
- package/template/saas-starter/src/app/(app)/app/page.tsx +76 -0
- package/template/saas-starter/src/app/(auth)/(account)/layout.tsx +64 -0
- package/template/saas-starter/src/app/(auth)/(account)/profile/page.tsx +16 -0
- package/template/saas-starter/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
- package/template/saas-starter/src/app/(auth)/(account)/support/[ticketId]/page.tsx +19 -0
- package/template/saas-starter/src/app/(auth)/(account)/support/page.tsx +9 -0
- package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +131 -0
- package/template/saas-starter/src/app/(auth)/login/page.tsx +105 -0
- package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +137 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +94 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +153 -0
- package/template/saas-starter/src/app/(invitation)/actions/invitations.ts +32 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/actions.ts +23 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/contact-form.tsx +118 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/page.tsx +20 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/validation.ts +22 -0
- package/template/saas-starter/src/app/(marketing)/layout.tsx +48 -0
- package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
- package/template/saas-starter/src/app/(marketing)/page.tsx +24 -0
- package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +63 -0
- package/template/saas-starter/src/app/(marketing)/terms/page.tsx +41 -0
- package/template/saas-starter/src/app/actions/support-tickets.ts +510 -0
- package/template/saas-starter/src/app/actions/support-tickets.validation.ts +112 -0
- package/template/saas-starter/src/app/actions/uploads.ts +36 -0
- package/template/saas-starter/src/app/actions/uploads.validation.ts +24 -0
- package/template/saas-starter/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/saas-starter/src/app/api/webhooks/stripe/route.ts +94 -0
- package/template/saas-starter/src/app/error.tsx +37 -0
- package/template/saas-starter/src/app/favicon.ico +0 -0
- package/template/saas-starter/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
- package/template/saas-starter/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
- package/template/saas-starter/src/app/fonts/index.ts +60 -0
- package/template/saas-starter/src/app/global-error.tsx +38 -0
- package/template/saas-starter/src/app/globals.css +269 -0
- package/template/saas-starter/src/app/layout.tsx +77 -0
- package/template/saas-starter/src/app/not-found.tsx +49 -0
- package/template/saas-starter/src/app/robots.ts +36 -0
- package/template/saas-starter/src/app/sitemap.ts +21 -0
- package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
- package/template/saas-starter/src/components/dashboard-header.tsx +70 -0
- package/template/saas-starter/src/components/data-table/enum.ts +5 -0
- package/template/saas-starter/src/components/data-table/features.ts +49 -0
- package/template/saas-starter/src/components/data-table/index.tsx +961 -0
- package/template/saas-starter/src/components/data-table/types.ts +76 -0
- package/template/saas-starter/src/components/error.tsx +71 -0
- package/template/saas-starter/src/components/file-upload.tsx +254 -0
- package/template/saas-starter/src/components/loader.tsx +41 -0
- package/template/saas-starter/src/components/logo.tsx +49 -0
- package/template/saas-starter/src/components/navigation-search.tsx +140 -0
- package/template/saas-starter/src/components/navigation-sidebar.tsx +241 -0
- package/template/saas-starter/src/components/page-heading.tsx +38 -0
- package/template/saas-starter/src/components/react-query-provider.tsx +51 -0
- package/template/saas-starter/src/components/route-error.tsx +55 -0
- package/template/saas-starter/src/components/screen-size.tsx +76 -0
- package/template/saas-starter/src/components/site-footer.tsx +39 -0
- package/template/saas-starter/src/components/support/create-support-ticket-dialog.tsx +122 -0
- package/template/saas-starter/src/components/support/support-ticket-conversation.tsx +236 -0
- package/template/saas-starter/src/components/support/support-ticket-shared.tsx +58 -0
- package/template/saas-starter/src/components/support/support-tickets-table.tsx +281 -0
- package/template/saas-starter/src/components/theme-provider.tsx +115 -0
- package/template/saas-starter/src/components/theme-toggle.tsx +75 -0
- package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
- package/template/saas-starter/src/components/ui/avatar.tsx +112 -0
- package/template/saas-starter/src/components/ui/badge.tsx +54 -0
- package/template/saas-starter/src/components/ui/button.tsx +69 -0
- package/template/saas-starter/src/components/ui/card.tsx +103 -0
- package/template/saas-starter/src/components/ui/checkbox.tsx +32 -0
- package/template/saas-starter/src/components/ui/collapsible.tsx +33 -0
- package/template/saas-starter/src/components/ui/command.tsx +192 -0
- package/template/saas-starter/src/components/ui/dialog.tsx +164 -0
- package/template/saas-starter/src/components/ui/dropdown-menu.tsx +273 -0
- package/template/saas-starter/src/components/ui/field.tsx +238 -0
- package/template/saas-starter/src/components/ui/input-group.tsx +156 -0
- package/template/saas-starter/src/components/ui/input.tsx +19 -0
- package/template/saas-starter/src/components/ui/label.tsx +24 -0
- package/template/saas-starter/src/components/ui/popover.tsx +89 -0
- package/template/saas-starter/src/components/ui/select.tsx +195 -0
- package/template/saas-starter/src/components/ui/separator.tsx +28 -0
- package/template/saas-starter/src/components/ui/sheet.tsx +143 -0
- package/template/saas-starter/src/components/ui/sidebar.tsx +702 -0
- package/template/saas-starter/src/components/ui/skeleton.tsx +13 -0
- package/template/saas-starter/src/components/ui/sonner.tsx +45 -0
- package/template/saas-starter/src/components/ui/switch.tsx +33 -0
- package/template/saas-starter/src/components/ui/table.tsx +116 -0
- package/template/saas-starter/src/components/ui/tabs.tsx +90 -0
- package/template/saas-starter/src/components/ui/textarea.tsx +18 -0
- package/template/saas-starter/src/components/ui/tooltip.tsx +57 -0
- package/template/saas-starter/src/components/user-menu.tsx +213 -0
- package/template/saas-starter/src/db/common.ts +63 -0
- package/template/saas-starter/src/db/enums.ts +50 -0
- package/template/saas-starter/src/db/index.ts +60 -0
- package/template/saas-starter/src/db/relations.ts +112 -0
- package/template/saas-starter/src/db/schema.ts +342 -0
- package/template/saas-starter/src/emails/components/email-layout.tsx +79 -0
- package/template/saas-starter/src/emails/invitation.tsx +69 -0
- package/template/saas-starter/src/emails/magic-link.tsx +51 -0
- package/template/saas-starter/src/emails/test.tsx +34 -0
- package/template/saas-starter/src/emails/welcome.tsx +37 -0
- package/template/saas-starter/src/env.ts +66 -0
- package/template/saas-starter/src/hooks/use-mobile.ts +21 -0
- package/template/saas-starter/src/instrumentation.ts +44 -0
- package/template/saas-starter/src/lib/actions.ts +55 -0
- package/template/saas-starter/src/lib/admin-navigation.ts +63 -0
- package/template/saas-starter/src/lib/app-navigation.ts +63 -0
- package/template/saas-starter/src/lib/auth-client.ts +34 -0
- package/template/saas-starter/src/lib/auth-permissions.ts +61 -0
- package/template/saas-starter/src/lib/auth.ts +312 -0
- package/template/saas-starter/src/lib/billing.ts +152 -0
- package/template/saas-starter/src/lib/callback-url.ts +28 -0
- package/template/saas-starter/src/lib/cn.ts +4 -0
- package/template/saas-starter/src/lib/constants.ts +91 -0
- package/template/saas-starter/src/lib/delay.ts +12 -0
- package/template/saas-starter/src/lib/email-service.ts +118 -0
- package/template/saas-starter/src/lib/errors.ts +74 -0
- package/template/saas-starter/src/lib/format-currency.ts +25 -0
- package/template/saas-starter/src/lib/format-date.ts +75 -0
- package/template/saas-starter/src/lib/image-url.ts +54 -0
- package/template/saas-starter/src/lib/nodemailer.ts +67 -0
- package/template/saas-starter/src/lib/organization-access.ts +73 -0
- package/template/saas-starter/src/lib/otel.ts +117 -0
- package/template/saas-starter/src/lib/plan-limits.ts +37 -0
- package/template/saas-starter/src/lib/plans.ts +90 -0
- package/template/saas-starter/src/lib/rbac.ts +44 -0
- package/template/saas-starter/src/lib/safe-action-client.ts +126 -0
- package/template/saas-starter/src/lib/safe-action.ts +187 -0
- package/template/saas-starter/src/lib/search.ts +16 -0
- package/template/saas-starter/src/lib/seo.ts +7 -0
- package/template/saas-starter/src/lib/siteConfig.ts +20 -0
- package/template/saas-starter/src/lib/storage.ts +311 -0
- package/template/saas-starter/src/lib/stripe.ts +9 -0
- package/template/saas-starter/src/lib/toast-message.ts +101 -0
- package/template/saas-starter/src/lib/validations.ts +95 -0
- package/template/saas-starter/src/proxy.ts +48 -0
- package/template/saas-starter/src/scripts/cleanup-temp-uploads.ts +24 -0
- package/template/saas-starter/src/scripts/reset-db.ts +22 -0
- package/template/saas-starter/src/scripts/seed.ts +85 -0
- package/template/saas-starter/src/scripts/setup-db.ts +72 -0
- package/template/saas-starter/src/scripts/setup-stripe-plans.ts +206 -0
- package/template/saas-starter/src/scripts/test-mail.ts +43 -0
- package/template/saas-starter/tests/admin/users.test.ts +64 -0
- package/template/saas-starter/tests/db/fk-indexes.test.ts +62 -0
- package/template/saas-starter/tests/db/seed.test.ts +57 -0
- package/template/saas-starter/tests/helpers/action.ts +71 -0
- package/template/saas-starter/tests/helpers/fixtures.ts +151 -0
- package/template/saas-starter/tests/helpers/index.ts +2 -0
- package/template/saas-starter/tests/lib/callback-url.test.ts +48 -0
- package/template/saas-starter/tests/lib/client-server-boundary.test.ts +57 -0
- package/template/saas-starter/tests/lib/errors.test.ts +10 -0
- package/template/saas-starter/tests/lib/image-url.test.ts +31 -0
- package/template/saas-starter/tests/lib/search.test.ts +23 -0
- package/template/saas-starter/tests/marketing/contact-and-feedback.test.ts +86 -0
- package/template/saas-starter/tests/organizations/organizations.test.ts +175 -0
- package/template/saas-starter/tests/setup/pglite-db.ts +67 -0
- package/template/saas-starter/tests/setup/preload.ts +108 -0
- package/template/saas-starter/tests/support/support-tickets.test.ts +145 -0
- package/template/saas-starter/tsconfig.json +52 -0
- package/cli.js +0 -143
- package/template/nextjs/src/app/(admin)/layout.tsx +0 -20
- package/template/nextjs/src/app/(app)/layout.tsx +0 -20
- package/template/nextjs/terraform/artifact-registry.tf +0 -5
- package/template/nextjs/terraform/backend-service.tf +0 -13
- package/template/nextjs/terraform/cleanup.sh +0 -9
- package/template/nextjs/terraform/cloudbuild.tf +0 -70
- package/template/nextjs/terraform/cloudrun.tf +0 -68
- package/template/nextjs/terraform/database.tf +0 -60
- package/template/nextjs/terraform/forwarding-rule.tf +0 -43
- package/template/nextjs/terraform/gitignore +0 -4
- package/template/nextjs/terraform/ip-address.tf +0 -11
- package/template/nextjs/terraform/network-endpoint-group.tf +0 -11
- package/template/nextjs/terraform/network.tf +0 -21
- package/template/nextjs/terraform/pre.sh +0 -3
- package/template/nextjs/terraform/providers.tf +0 -18
- package/template/nextjs/terraform/secret.txt +0 -11
- package/template/nextjs/terraform/setup.sh +0 -25
- package/template/nextjs/terraform/ssl-certs.tf +0 -32
- package/template/nextjs/terraform/target-https-proxy copy.tf +0 -14
- package/template/nextjs/terraform/target-https-proxy.tf +0 -14
- package/template/nextjs/terraform/terraform.tfvars +0 -4
- package/template/nextjs/terraform/url-map.tf +0 -23
- package/template/nextjs/terraform/variables.tf +0 -21
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Shared guidance for agentic coding tools working in this repository.
|
|
4
|
+
|
|
5
|
+
## What is this project
|
|
6
|
+
|
|
7
|
+
{{PROJECT_DESCRIPTION}}
|
|
8
|
+
|
|
9
|
+
It is a multi-tenant SaaS built on the `saas-starter` template from `create-ej-app`. People sign in, create or join organizations, invite teammates, and pay per organization through Stripe. Platform admins run the product from an admin panel.
|
|
10
|
+
|
|
11
|
+
## Coding preferences
|
|
12
|
+
|
|
13
|
+
- Keep things simple. Channel "yagni" energy unless told otherwise.
|
|
14
|
+
- Don't export a function or type, or define a type, that nothing uses.
|
|
15
|
+
- Type safety is useful; take advantage of it.
|
|
16
|
+
- Don't write code that introduces security vulnerabilities. Include authorization checks and input validation, and follow security best practices.
|
|
17
|
+
- Don't be scared to propose bold ideas or breaking changes if they can meaningfully benefit our work.
|
|
18
|
+
- Be careful with destructive actions I haven't explicitly requested.
|
|
19
|
+
- Write performant, optimized code.
|
|
20
|
+
- Don't hardcode limits, labels, or repeated values. Put them in `src/lib/constants.ts` or the matching registry, and read them from there in the UI, validation, tests and docs so one change updates every place.
|
|
21
|
+
- Tests are good! Endless smoke tests and "regression tests" for feature deletions are much less good. Tests should be focused, not slop.
|
|
22
|
+
- Comments are a great way to clarify functionality and how code is used. Don't comment every line. Concisely describe how functions and classes are used above their definitions, and explain only the tricky lines.
|
|
23
|
+
- If you need a paragraph-long comment to justify why a workaround is OK, the code is wrong. Fix the code unless I tell you otherwise.
|
|
24
|
+
- When designing or building something new, ask me about any design or flow decision you're unsure about, unless the answer is obvious. I'd rather clarify upfront than redo work built on an assumption. When you ask, give your recommendation and suggested answers so I can confirm, redirect, or decide.
|
|
25
|
+
- Report any bug, problem, or security issue you notice anywhere in the codebase, even if it's unrelated to the current work. Don't silently fix or ignore it.
|
|
26
|
+
|
|
27
|
+
## Writing guidelines
|
|
28
|
+
|
|
29
|
+
These cover every word that ships: UI copy, dialog and page titles, toasts, validation and error messages, emails, code comments, commit messages, PR descriptions, and Markdown in the repo.
|
|
30
|
+
|
|
31
|
+
**Fix the English you notice.** Typos, awkward phrasing, and copy that breaks these rules get fixed on sight, even outside the task you were given. This is the one exception to the reporting rule above. Bugs and security issues are still reported rather than silently changed; wording is not.
|
|
32
|
+
|
|
33
|
+
### Casing
|
|
34
|
+
|
|
35
|
+
Title Case for headings, page and section titles, dialog titles, nav labels, button labels, and table column headers: `Support Tickets`, `Save Changes`, not `Support tickets` or `Save changes`. Sentence case for body copy, descriptions, field labels, helper text, validation messages, and Markdown headings in repo docs.
|
|
36
|
+
|
|
37
|
+
### Voice
|
|
38
|
+
|
|
39
|
+
- Active voice, present tense. Append "by monkeys" as a test: if the sentence still parses, rewrite it.
|
|
40
|
+
- Address the reader as `you`, never "the user".
|
|
41
|
+
- Imperative for actions: "Select a plan", not "You will need to select a plan".
|
|
42
|
+
- Aim under 20 words per sentence. Paragraphs of 2 to 4 sentences, one idea each. Contractions are welcome.
|
|
43
|
+
- No rhetorical questions.
|
|
44
|
+
- Don't open a paragraph by recapping the previous one: "With that in place…".
|
|
45
|
+
- Don't have machines perform human actions: "the browser fetches the URL", not "hand the browser a URL".
|
|
46
|
+
- Commit subjects are lowercase imperative sentences.
|
|
47
|
+
- Don't include `Co-authored-by` or similar attribution trailers in commit messages or descriptions.
|
|
48
|
+
- Avoid repeating the same information across UI copy on a page. Give each heading, description, and helper text a distinct purpose.
|
|
49
|
+
|
|
50
|
+
### Words to cut
|
|
51
|
+
|
|
52
|
+
- `easy`, `simple`, `quick`: name the concrete thing instead, such as "one command" or "default settings".
|
|
53
|
+
- `very`, `just`, `really`, `simply`: filler.
|
|
54
|
+
- Vague quantifiers such as `many`, `often`, `typically`, `significantly`, `near-zero`, and `sub-second`: give the number or the specific claim.
|
|
55
|
+
- Metaphor verbs such as `lands`, `carries`, `hits`, and `flows through`: name the literal action.
|
|
56
|
+
- Spec-sheet phrasing: "provides", "is configurable", "is explicitly labeled".
|
|
57
|
+
|
|
58
|
+
### Punctuation and units
|
|
59
|
+
|
|
60
|
+
- No em dashes or curly quotes. Use a colon, a comma, or a period, or rewrite the sentence.
|
|
61
|
+
- Ellipsis character `…`, never three dots. Loading states end with it: `Saving…`.
|
|
62
|
+
- Units take a space and an uppercase symbol: `64 KB`, `200 ms`. Seconds stay bare: `30s`.
|
|
63
|
+
|
|
64
|
+
### Emphasis and code
|
|
65
|
+
|
|
66
|
+
- Bold marks a UI element or a critical fact, never emphasis.
|
|
67
|
+
- Inline code for paths, identifiers, file extensions, and short snippets.
|
|
68
|
+
- Code blocks carry a language tag, stay under 25 lines, and are explained in prose.
|
|
69
|
+
- Placeholders are descriptive `snake_case`: `your_access_token_here`, never `<TOKEN>` or `xxx`.
|
|
70
|
+
|
|
71
|
+
### Lists
|
|
72
|
+
|
|
73
|
+
- Introduce every list with a colon.
|
|
74
|
+
- Three or more list-shaped items in a paragraph become a list.
|
|
75
|
+
- No trailing period on a fragment; keep it on a full sentence.
|
|
76
|
+
- Bold term then colon for definitions: `- **Term**: description`.
|
|
77
|
+
|
|
78
|
+
### Error and toast messages
|
|
79
|
+
|
|
80
|
+
Name the subject, the action, and the consequence. No apologies. For customer-facing copy, say what happened and what to do next: "Your plan allows 25 members. Upgrade your plan to invite more." Never "Error" or "Something went wrong" on its own. Keep internals out of anything a customer reads: no table names, stack traces, or raw IDs.
|
|
81
|
+
|
|
82
|
+
## Commands
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
bun run dev # Start Next.js dev server
|
|
86
|
+
bun run build # Production build
|
|
87
|
+
bun run lint # ESLint
|
|
88
|
+
bun run type-check # TypeScript check (tsgo)
|
|
89
|
+
bun run format # Prettier
|
|
90
|
+
|
|
91
|
+
bun run db:push # Install triggers, then apply the schema
|
|
92
|
+
bun run db:studio # Open Drizzle Studio
|
|
93
|
+
bun run db:seed # Seed a superadmin, a user, and one organization
|
|
94
|
+
bun run db:reset # Drop every table (refuses in production)
|
|
95
|
+
|
|
96
|
+
bun test # Bun tests against in-memory PGlite
|
|
97
|
+
bun run test:real # Same suite against real Postgres (TEST_DATABASE=postgres)
|
|
98
|
+
bun run email:dev # React Email preview server (src/emails/)
|
|
99
|
+
bun run email:test you@example.com # Send one test email
|
|
100
|
+
bun run storage:cleanup # Delete abandoned R2 uploads; schedule it
|
|
101
|
+
|
|
102
|
+
bun run stripe:setup # Create Stripe products, prices, and the portal config
|
|
103
|
+
bun run stripe:dev # Forward Stripe webhooks to localhost
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Do not start the dev server or any other long-running process unless asked.
|
|
107
|
+
|
|
108
|
+
After editing code, run `bunx prettier --write` on the files you touched, or `bun run format` after a broad refactor.
|
|
109
|
+
|
|
110
|
+
## Environment variables
|
|
111
|
+
|
|
112
|
+
All app env is defined and validated in [src/env.ts](src/env.ts) via `@t3-oss/env-nextjs`. Add a variable there and to `.env.example` in the same change.
|
|
113
|
+
|
|
114
|
+
## Architecture
|
|
115
|
+
|
|
116
|
+
**Stack:** Next.js 16 App Router, React 19, TypeScript, Drizzle ORM (PostgreSQL), Better Auth (admin and organization plugins), Stripe, shadcn/ui, Tailwind CSS 4, Zod, next-safe-action, TanStack Query, Workflow, React Email.
|
|
117
|
+
|
|
118
|
+
**Path alias:** `~/` maps to `src/`.
|
|
119
|
+
|
|
120
|
+
### Route groups
|
|
121
|
+
|
|
122
|
+
| Group | URL prefix | Purpose |
|
|
123
|
+
| -------------- | ------------------------- | ----------------------------------------- |
|
|
124
|
+
| `(marketing)` | `/` | Landing, contact, and legal pages |
|
|
125
|
+
| `(auth)` | `/login`, `/profile` | Sign-in, profile, and account support |
|
|
126
|
+
| `(app)` | `/app/...` | Organization workspace, settings, billing |
|
|
127
|
+
| `(admin)` | `/admin/...` | Platform admin panel |
|
|
128
|
+
| `(invitation)` | `/accept-invitation/[id]` | Accept or decline an organization invite |
|
|
129
|
+
|
|
130
|
+
### Server actions
|
|
131
|
+
|
|
132
|
+
All server actions use `next-safe-action` with layered clients defined in [src/lib/safe-action.ts](src/lib/safe-action.ts):
|
|
133
|
+
|
|
134
|
+
- `actionClient`: injects the auth session into `ctx`; no authentication required
|
|
135
|
+
- `authActionClient`: requires an authenticated user
|
|
136
|
+
- `adminActionClient`: requires the `admin` or `superadmin` system role
|
|
137
|
+
- `superAdminActionClient`: requires the `superadmin` system role
|
|
138
|
+
|
|
139
|
+
Actions live beside their routes in `actions/` directories; actions shared across route groups live in `src/app/actions/`. Zod schemas for actions live in co-located `.validation.ts` files so client forms can import them.
|
|
140
|
+
|
|
141
|
+
No page or layout under `src/app` imports `~/db` directly. Fetch through an action whose client enforces the right role, then call `notFound()` when it returns no data. An ancestor layout check is not enough: the `/admin` layout checks only `isAdmin`, so any page trusting it stays open to a plain admin.
|
|
142
|
+
|
|
143
|
+
### Errors (`AppError`)
|
|
144
|
+
|
|
145
|
+
[src/lib/errors.ts](src/lib/errors.ts). Throw `AppError` for expected failures (auth, not-found, conflicts, business rules). `message` is customer-facing (what happened + next step); no internals. Use `rethrowUniqueViolation` to turn a unique-index failure into a readable conflict.
|
|
146
|
+
|
|
147
|
+
`handleServerError` in [src/lib/safe-action.ts](src/lib/safe-action.ts): `AppError` → client gets `message`; anything else → generic mask + `recordError`. Do not catch-and-rethrow as plain `Error` or return `null` on failure; let errors bubble. Try/catch only for non-fatal side effects.
|
|
148
|
+
|
|
149
|
+
### Observability
|
|
150
|
+
|
|
151
|
+
One structured span per action, with its input, duration, and outcome as attributes. Setup is in [src/instrumentation.ts](src/instrumentation.ts) and [src/lib/otel.ts](src/lib/otel.ts); add attributes with `recordWideEvent` and `recordError`. Only error spans and spans slower than 5s are exported.
|
|
152
|
+
|
|
153
|
+
Wrap new actions with `withWideEvent` from [src/lib/safe-action.ts](src/lib/safe-action.ts): `.action(withWideEvent('myAction', async ({ parsedInput, ctx }) => { … }))`. Prefer that over ad-hoc logging in handlers. No secrets in attributes.
|
|
154
|
+
|
|
155
|
+
### Client toasts
|
|
156
|
+
|
|
157
|
+
[src/lib/toast-message.ts](src/lib/toast-message.ts) only (not raw Sonner):
|
|
158
|
+
|
|
159
|
+
- `await action`: `if (toastIfActionFailed(result)) return` then `toastSuccessMessage`
|
|
160
|
+
- hook `onError`, or a Better Auth client `error`: `toastActionError(err, 'The thing was not saved. Try again.')`
|
|
161
|
+
- client-only validation: `toastErrorMessage`
|
|
162
|
+
|
|
163
|
+
Query/mutation helpers: [src/lib/safe-action-client.ts](src/lib/safe-action-client.ts). Confirm destructive actions with `ConfirmDialog` ([src/components/confirm-dialog.tsx](src/components/confirm-dialog.tsx)), never `window.confirm`.
|
|
164
|
+
|
|
165
|
+
### Client / server boundary
|
|
166
|
+
|
|
167
|
+
`'use client'` modules must never pull server runtime, secrets, or the database into the browser bundle. Transitive imports count: a Zod schema that pulls in `drizzle-orm` still ships drizzle.
|
|
168
|
+
|
|
169
|
+
**Never import from client:**
|
|
170
|
+
|
|
171
|
+
- `~/db` and anything under it (including for types)
|
|
172
|
+
- `~/env`
|
|
173
|
+
- `drizzle-orm`
|
|
174
|
+
- Any module with `import 'server-only'`, such as `~/lib/plans`, `~/lib/billing`, `~/lib/storage`, and `~/lib/organization-access`
|
|
175
|
+
|
|
176
|
+
**OK from client:**
|
|
177
|
+
|
|
178
|
+
- Server actions from `'use server'` modules
|
|
179
|
+
- Pure modules free of SQL and secrets: `constants`, `validations`, `image-url`, `rbac`, `format-*`
|
|
180
|
+
- Action `.validation.ts` files, as long as they only import client-safe modules
|
|
181
|
+
|
|
182
|
+
**`server-only` placement:** add it to modules that open DB connections or hold secrets or must never ship to the client. Do not add it to `~/env`, `~/db/schema`, `~/db/enums`, or `~/db/common`: drizzle-kit and scripts load those without the `react-server` condition, and the real package throws there. Scripts run with `bun --conditions=react-server` so they can import server-only modules.
|
|
183
|
+
|
|
184
|
+
[tests/lib/client-server-boundary.test.ts](tests/lib/client-server-boundary.test.ts) fails if a `'use client'` file imports a forbidden path. Keep it green; extend its list when you add a new server lib.
|
|
185
|
+
|
|
186
|
+
### Input bounds and search
|
|
187
|
+
|
|
188
|
+
Shared Zod helpers live in [src/lib/validations.ts](src/lib/validations.ts); numeric ceilings live in [src/lib/constants.ts](src/lib/constants.ts). Cap every array that reaches SQL with `MAX_FILTER_VALUES` or a named constant. Build `ILIKE` patterns with `likeContains` from [src/lib/search.ts](src/lib/search.ts) so `%` and `_` in search text stay literal.
|
|
189
|
+
|
|
190
|
+
### Two-tier RBAC
|
|
191
|
+
|
|
192
|
+
**System level** (Better Auth admin plugin, defined in [src/lib/auth-permissions.ts](src/lib/auth-permissions.ts)):
|
|
193
|
+
|
|
194
|
+
- `user`: regular customer with no admin-panel access
|
|
195
|
+
- `admin`: can open `/admin` and work with support tickets, feedback, and contact leads; cannot manage users
|
|
196
|
+
- `superadmin`: full control, including user management and impersonation
|
|
197
|
+
|
|
198
|
+
**Organization level** (Better Auth organization plugin, labels and predicates in [src/lib/rbac.ts](src/lib/rbac.ts)):
|
|
199
|
+
|
|
200
|
+
- `owner`: full control, including deletion and ownership transfer
|
|
201
|
+
- `admin`: manages settings, billing, and members, but not ownership
|
|
202
|
+
- `member`: uses the workspace
|
|
203
|
+
|
|
204
|
+
Assert organization access in actions with helpers from [src/lib/organization-access.ts](src/lib/organization-access.ts): `assertMember`, `assertRole`, `assertCanManageOrganization`. Better Auth enforces the same roles on its own organization endpoints.
|
|
205
|
+
|
|
206
|
+
Organization logos never go through the Better Auth client: its hooks reject a `logo` field, and `updateOrganizationAction` saves it after checking the upload belongs to the organization.
|
|
207
|
+
|
|
208
|
+
### Plans and billing
|
|
209
|
+
|
|
210
|
+
Plans live in [src/lib/plans.ts](src/lib/plans.ts); `getOrganizationPlan` in [src/lib/plan-limits.ts](src/lib/plan-limits.ts) returns the plan an organization is entitled to now. Only `trialing`, `active`, and `past_due` subscriptions keep a paid plan. Better Auth reads `maxMembers` for invitation and membership limits.
|
|
211
|
+
|
|
212
|
+
Stripe is the source of truth. Checkout and the billing portal are Stripe-hosted; [src/app/api/webhooks/stripe/route.ts](src/app/api/webhooks/stripe/route.ts) records each event once and calls `syncSubscriptionFromStripe`, which re-reads the customer's latest subscription instead of trusting the event payload. Deleting an organization cancels its Stripe subscription first.
|
|
213
|
+
|
|
214
|
+
### Database
|
|
215
|
+
|
|
216
|
+
Schema is all in [src/db/schema.ts](src/db/schema.ts). All enums live in [src/db/enums.ts](src/db/enums.ts). Reusable column helpers (CUID IDs, timestamps, CHECK helpers) are in [src/db/common.ts](src/db/common.ts). IDs use CUID2 with a model-name prefix (e.g., `user_xxx`, `organization_xxx`).
|
|
217
|
+
|
|
218
|
+
Functions and triggers Drizzle cannot manage (the `TICKET-000001` counter) live in [src/scripts/setup-db.ts](src/scripts/setup-db.ts); `db:push` and the test database both run it.
|
|
219
|
+
|
|
220
|
+
**Changing the schema:**
|
|
221
|
+
|
|
222
|
+
- Every foreign key must be the leading column of an index and must declare an explicit `onDelete`. Without an index, deleting a parent scans the child table while holding row locks. `tests/db/fk-indexes.test.ts` enforces both.
|
|
223
|
+
- Before adding a CHECK, read what the code and the seed script write to that column. Use `nonNegativeCheck` for counts and sizes.
|
|
224
|
+
- Better Auth owns the `users`, `sessions`, `accounts`, `verifications`, `organizations`, `members`, and `invitations` columns. Change them with `bun run gen:auth` in mind.
|
|
225
|
+
|
|
226
|
+
### File storage (R2)
|
|
227
|
+
|
|
228
|
+
Storage logic is in [src/lib/storage.ts](src/lib/storage.ts). When all five `R2_*` env vars are set, files upload directly to R2 via presigned PUT URLs with the content type signed. Otherwise the app stores base64 data URLs (dev mode).
|
|
229
|
+
|
|
230
|
+
Image DB fields hold an R2 object key (`uploads/organizations/<id>/<file>`) or a `data:image/*` URL, validated by `imageUrlValidation`. Before saving one:
|
|
231
|
+
|
|
232
|
+
- Call `assertPersistableImageValue`, which rejects data URLs while R2 is on.
|
|
233
|
+
- Call `assertOwnedR2Key` on any key from user input, so no request can use another organization's file.
|
|
234
|
+
- Call `confirmUpload(newKey, organizationId, oldKey)` after the save, which marks the upload permanent and deletes the replaced file.
|
|
235
|
+
|
|
236
|
+
Render with `resolveImageUrl`, which returns null for anything that is not a stored image. Uploads that are never confirmed stay temporary; `bun run storage:cleanup` deletes them, so run it on a schedule. The `FileUpload` component ([src/components/file-upload.tsx](src/components/file-upload.tsx)) handles the browser side; pass `currentUrl` from `resolveImageUrl`, and match `sizes` to the rendered width.
|
|
237
|
+
|
|
238
|
+
### Support, feedback, and contact
|
|
239
|
+
|
|
240
|
+
- **Support tickets** ([src/app/actions/support-tickets.ts](src/app/actions/support-tickets.ts)): a ticket with an `organizationId` is shared by that organization's owners and admins; a ticket without one is private to its author. Platform admins see and answer both; a reply counts as the support team's only when sent from the admin panel (`asSupport: true`).
|
|
241
|
+
- **Feedback**: any member rates the product from the header of an organization page.
|
|
242
|
+
- **Contact leads**: the public `/contact-us` form. Both inboxes appear in the admin panel.
|
|
243
|
+
- **Leaving**: owners and admins leave from Settings; members leave from the organization switcher. The last owner cannot leave or be deleted until ownership moves.
|
|
244
|
+
|
|
245
|
+
### Client data and forms
|
|
246
|
+
|
|
247
|
+
Query results are fresh object identities on every refetch, and TanStack refetches on window focus by default. An effect that depends on one re-runs on every alt-tab and discards typed input.
|
|
248
|
+
|
|
249
|
+
- Seed a dialog form once per open: depend on `[open, record?.id, reset]`, never on the record object, an options array, or a `useMemo` derived from them.
|
|
250
|
+
- When the data arrives after the dialog opens, guard the prefill with a ref that is cleared on close.
|
|
251
|
+
- These effects need `// eslint-disable-next-line react-hooks/exhaustive-deps` on the dependency array, with a comment saying why.
|
|
252
|
+
- Writing a ref during render is a React Compiler error here. Do it inside the effect.
|
|
253
|
+
|
|
254
|
+
### Email
|
|
255
|
+
|
|
256
|
+
Templates are React Email components in `src/emails/`. Send via helpers in [src/lib/email-service.ts](src/lib/email-service.ts), which run as Workflow steps and use Nodemailer. Call them with `start(sendXEmail, [...])` so a slow SMTP server never blocks a request.
|
|
257
|
+
|
|
258
|
+
### Tests
|
|
259
|
+
|
|
260
|
+
**Test-driven development (TDD):** For new features, bugs, or behavior changes, write or update focused tests (including edge cases).
|
|
261
|
+
|
|
262
|
+
Bun tests run against **in-memory Postgres via PGlite** by default. The preload ([tests/setup/preload.ts](tests/setup/preload.ts)) boots one PGlite instance per worker, applies the Drizzle schema plus `setup-db` triggers, mocks `~/db`, and mocks auth (`getAuthSession` via AsyncLocalStorage). Bootstrap: [tests/setup/pglite-db.ts](tests/setup/pglite-db.ts).
|
|
263
|
+
|
|
264
|
+
Tests read env from `.env`, like the app. `bun run test:real` runs the same suite against a real server; it requires `DATABASE_URL` and a schema applied with `db:push`.
|
|
265
|
+
|
|
266
|
+
**PGlite constraint:** PGlite has a single connection, so calling the outer `db` client (or any helper that uses it) inside `db.transaction(...)` deadlocks. Run checks before opening the transaction, or pass `tx` into loaders.
|
|
267
|
+
|
|
268
|
+
**Verify a test can fail:** after writing one, break the code it covers, confirm it goes red, then restore. A test that cannot fail reads as coverage and hides bugs.
|
|
269
|
+
|
|
270
|
+
**Tests must not depend on the machine clock.** Pin the current time with `setSystemTime` from `bun:test` rather than computing offsets from the real clock.
|
|
271
|
+
|
|
272
|
+
**`db.execute` result shape differs by driver:** postgres-js returns a row array, PGlite returns `{ rows }`. Normalise before reading, as `toRows` in `tests/db/fk-indexes.test.ts` does.
|
|
273
|
+
|
|
274
|
+
**Fixtures** (full API in [tests/helpers/fixtures.ts](tests/helpers/fixtures.ts)):
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
const fx = new Fixtures()
|
|
278
|
+
afterEach(() => fx.cleanup())
|
|
279
|
+
|
|
280
|
+
const ws = await fx.workspace({ plan: 'pro' }) // organization + owner/admin/member
|
|
281
|
+
await asUser(ws.owner, () => someAction(...))
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Use `asUser` / `asAnonymous` to set the action actor. Suites live under `tests/organizations/`, `tests/support/`, `tests/admin/`, `tests/db/`, and `tests/lib/`.
|
|
285
|
+
|
|
286
|
+
### Legal pages
|
|
287
|
+
|
|
288
|
+
The privacy policy and terms of service live in `src/app/(marketing)`. They are starting points: read both when you change how the product behaves. If a change makes either page inaccurate or incomplete, update it and `siteConfig.legal.lastUpdated` in the same change. Describe what an end user experiences, never internal architecture.
|
|
289
|
+
|
|
290
|
+
### Branding
|
|
291
|
+
|
|
292
|
+
Never hardcode the product name. Read `siteConfig.name` from [src/lib/siteConfig.ts](src/lib/siteConfig.ts). Replace `public/logo.svg` and `public/logo-dark.svg` with the real mark, and set the brand color in `src/app/globals.css` (`--primary`) and `BRAND` in `src/emails/components/email-layout.tsx`.
|
|
293
|
+
|
|
294
|
+
<!-- BEGIN:nextjs-agent-rules -->
|
|
295
|
+
|
|
296
|
+
# This is NOT the Next.js you know
|
|
297
|
+
|
|
298
|
+
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
|
299
|
+
|
|
300
|
+
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
|
301
|
+
|
|
302
|
+
<!-- END:nextjs-agent-rules -->
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
{{PROJECT_DESCRIPTION}}
|
|
4
|
+
|
|
5
|
+
## Technology
|
|
6
|
+
|
|
7
|
+
The application uses:
|
|
8
|
+
|
|
9
|
+
- **Application**: Next.js, React, and TypeScript
|
|
10
|
+
- **Interface**: Tailwind CSS, shadcn/ui, and TanStack Query
|
|
11
|
+
- **Data**: PostgreSQL and Drizzle ORM
|
|
12
|
+
- **Authentication**: Better Auth with system roles and organizations
|
|
13
|
+
- **Billing**: Stripe Checkout, the billing portal, and webhooks
|
|
14
|
+
- **Validation and actions**: Zod and next-safe-action
|
|
15
|
+
- **Runtime and tests**: Bun and PGlite
|
|
16
|
+
- **Storage and email**: Cloudflare R2, Nodemailer, and React Email
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
Next.js route groups separate each product surface:
|
|
21
|
+
|
|
22
|
+
| Directory | Route | Purpose |
|
|
23
|
+
| ---------------------- | ---------------------- | ----------------------------------------- |
|
|
24
|
+
| `src/app/(marketing)` | `/` | Landing, contact, and legal pages |
|
|
25
|
+
| `src/app/(auth)` | `/login`, `/profile` | Sign-in, profile, and account support |
|
|
26
|
+
| `src/app/(app)` | `/app/*` | Organization workspace, settings, billing |
|
|
27
|
+
| `src/app/(admin)` | `/admin/*` | Platform administration |
|
|
28
|
+
| `src/app/(invitation)` | `/accept-invitation/*` | Organization invitations |
|
|
29
|
+
|
|
30
|
+
Shared code has clear responsibilities:
|
|
31
|
+
|
|
32
|
+
- `src/components`: shared interface components
|
|
33
|
+
- `src/lib`: access checks, billing, storage, validation, and integrations
|
|
34
|
+
- `src/db`: schema, relations, enums, and database connection
|
|
35
|
+
- `src/emails`: React Email templates
|
|
36
|
+
- `tests`: focused integration and unit tests
|
|
37
|
+
|
|
38
|
+
Server actions sit beside their routes in `actions` directories. Each protected action validates input, enforces the correct role, and records one structured event. Page and layout files fetch data through actions instead of importing the database.
|
|
39
|
+
|
|
40
|
+
## Local setup
|
|
41
|
+
|
|
42
|
+
Install these prerequisites:
|
|
43
|
+
|
|
44
|
+
- [Bun](https://bun.sh/)
|
|
45
|
+
- [PostgreSQL](https://www.postgresql.org/) with an empty local database
|
|
46
|
+
- OAuth credentials for GitHub and Google
|
|
47
|
+
- SMTP credentials
|
|
48
|
+
- A Stripe account and the [Stripe CLI](https://docs.stripe.com/stripe-cli)
|
|
49
|
+
|
|
50
|
+
Then prepare the application:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
bun install
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`create-ej-app` already wrote `.env` from `.env.example`, with a random `BETTER_AUTH_SECRET`. Fill in working credentials.
|
|
57
|
+
|
|
58
|
+
Cloudflare R2 variables are optional. Without them, uploads are stored in the database as data URLs, so set them before production. With R2 on, schedule `bun run storage:cleanup` to delete abandoned uploads.
|
|
59
|
+
|
|
60
|
+
Create the Stripe products and prices, then copy the IDs from `stripe.txt` into `.env`:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bun run stripe:setup
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Apply the database schema and add starter data:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
bun run db:push
|
|
70
|
+
bun run db:seed
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Start the development server, and forward Stripe webhooks in a second terminal:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bun run dev
|
|
77
|
+
bun run stripe:dev
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Open [http://localhost:3000](http://localhost:3000). The seed creates `superadmin@example.com` and `user@example.com`; change them in `src/scripts/seed.ts` to addresses whose inbox you can read.
|
|
81
|
+
|
|
82
|
+
## Commands
|
|
83
|
+
|
|
84
|
+
Use these commands during development:
|
|
85
|
+
|
|
86
|
+
| Command | Purpose |
|
|
87
|
+
| ------------------------- | ----------------------------------- |
|
|
88
|
+
| `bun run dev` | Start the development server |
|
|
89
|
+
| `bun test` | Run tests with in-memory PostgreSQL |
|
|
90
|
+
| `bun run test:real` | Run tests against `DATABASE_URL` |
|
|
91
|
+
| `bun run lint` | Run ESLint |
|
|
92
|
+
| `bun run type-check` | Check TypeScript |
|
|
93
|
+
| `bun run format` | Format source and test files |
|
|
94
|
+
| `bun run build` | Create a production build |
|
|
95
|
+
| `bun run db:push` | Apply the Drizzle schema |
|
|
96
|
+
| `bun run db:studio` | Open Drizzle Studio |
|
|
97
|
+
| `bun run db:seed` | Add starter data |
|
|
98
|
+
| `bun run db:reset` | Drop every table |
|
|
99
|
+
| `bun run email:dev` | Preview email templates |
|
|
100
|
+
| `bun run email:test` | Send a test email to an address |
|
|
101
|
+
| `bun run storage:cleanup` | Delete abandoned uploads from R2 |
|
|
102
|
+
|
|
103
|
+
Before opening a pull request, run:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
bun test
|
|
107
|
+
bun run lint
|
|
108
|
+
bun run type-check
|
|
109
|
+
bun run build
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Implement a change
|
|
113
|
+
|
|
114
|
+
Follow this path for product changes:
|
|
115
|
+
|
|
116
|
+
1. Find the matching route under `src/app` and read its nearby actions and components.
|
|
117
|
+
2. Add or update a focused test before changing behavior.
|
|
118
|
+
3. Put shared limits and repeated values in `src/lib/constants.ts`.
|
|
119
|
+
4. Validate action input with Zod and select the narrowest action client in `src/lib/safe-action.ts`.
|
|
120
|
+
5. Enforce organization access with helpers from `src/lib/organization-access.ts`.
|
|
121
|
+
6. Throw `AppError` for expected failures and give customers a clear next step.
|
|
122
|
+
7. Wrap new actions with `withWideEvent` and exclude secrets from event attributes.
|
|
123
|
+
8. Format the files you touched, then run the relevant checks.
|
|
124
|
+
|
|
125
|
+
Read [AGENTS.md](AGENTS.md) before making a substantial change. It documents authorization, billing, file storage, testing, and writing rules.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "radix-vega",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"rtl": false,
|
|
15
|
+
"aliases": {
|
|
16
|
+
"components": "~/components",
|
|
17
|
+
"utils": "~/lib/cn",
|
|
18
|
+
"ui": "~/components/ui",
|
|
19
|
+
"lib": "~/lib",
|
|
20
|
+
"hooks": "~/hooks"
|
|
21
|
+
},
|
|
22
|
+
"menuColor": "default",
|
|
23
|
+
"menuAccent": "subtle",
|
|
24
|
+
"registries": {}
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import nextVitals from 'eslint-config-next/core-web-vitals'
|
|
2
|
+
import nextTs from 'eslint-config-next/typescript'
|
|
3
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
globalIgnores([
|
|
9
|
+
'node_modules/**',
|
|
10
|
+
'.next/**',
|
|
11
|
+
'out/**',
|
|
12
|
+
'build/**',
|
|
13
|
+
'next-env.d.ts',
|
|
14
|
+
// workflow-sdk autogenerated files
|
|
15
|
+
'src/app/.well-known/**'
|
|
16
|
+
]),
|
|
17
|
+
{
|
|
18
|
+
rules: {
|
|
19
|
+
'@typescript-eslint/no-explicit-any': 'off'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
export default eslintConfig
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import bundleAnalyzer from '@next/bundle-analyzer'
|
|
2
|
+
import type { NextConfig } from 'next'
|
|
3
|
+
import { withWorkflow } from 'workflow/next'
|
|
4
|
+
import { env } from '~/env'
|
|
5
|
+
import { MAX_IMAGE_URL_LENGTH } from '~/lib/constants'
|
|
6
|
+
|
|
7
|
+
const nextConfig: NextConfig = {
|
|
8
|
+
reactCompiler: true,
|
|
9
|
+
devIndicators: false,
|
|
10
|
+
experimental: {
|
|
11
|
+
serverActions: {
|
|
12
|
+
// Without R2, a logo reaches its server action as a base64 data URL.
|
|
13
|
+
// Next's 1 MB default would reject most of the images the upload allows.
|
|
14
|
+
bodySizeLimit: `${Math.ceil(MAX_IMAGE_URL_LENGTH / 1024 / 1024) + 1}mb`
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
images: {
|
|
18
|
+
remotePatterns: [
|
|
19
|
+
{ hostname: 'avatars.githubusercontent.com' },
|
|
20
|
+
{ hostname: 'lh3.googleusercontent.com' },
|
|
21
|
+
...(env.R2_PUBLIC_URL
|
|
22
|
+
? [{ hostname: new URL(env.R2_PUBLIC_URL).hostname }]
|
|
23
|
+
: [])
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const withBundleAnalyzer = bundleAnalyzer({
|
|
29
|
+
enabled: env.ANALYZE === 'true'
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export default withWorkflow(withBundleAnalyzer(nextConfig))
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-project",
|
|
3
|
+
"description": "A Legendary Project",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev",
|
|
8
|
+
"email:dev": "email dev --dir src/emails",
|
|
9
|
+
"email:test": "bun --conditions=react-server ./src/scripts/test-mail.ts",
|
|
10
|
+
"workflow:inspect": "workflow inspect runs --web",
|
|
11
|
+
"build": "bun --bun next build",
|
|
12
|
+
"build:docker": "docker build -t my-app .",
|
|
13
|
+
"analyze": "ANALYZE=true bun --bun next build --webpack",
|
|
14
|
+
"start": "bun --bun next start",
|
|
15
|
+
"lint": "eslint",
|
|
16
|
+
"type-check": "tsgo --incremental false",
|
|
17
|
+
"format": "prettier --log-level warn --write \"./src/**/*.{js,cjs,mjs,jsx,ts,tsx}\" --write \"./tests/**/*.{js,cjs,mjs,jsx,ts,tsx}\"",
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"test:real": "TEST_DATABASE=postgres bun test",
|
|
20
|
+
"db:push": "bun --conditions=react-server ./src/scripts/setup-db.ts && drizzle-kit push",
|
|
21
|
+
"db:seed": "bun --conditions=react-server ./src/scripts/seed.ts",
|
|
22
|
+
"db:reset": "bun --conditions=react-server ./src/scripts/reset-db.ts",
|
|
23
|
+
"db:studio": "drizzle-kit studio",
|
|
24
|
+
"gen:auth": "bunx auth@latest generate",
|
|
25
|
+
"storage:cleanup": "bun --conditions=react-server ./src/scripts/cleanup-temp-uploads.ts",
|
|
26
|
+
"stripe:setup": "bun --conditions=react-server ./src/scripts/setup-stripe-plans.ts",
|
|
27
|
+
"stripe:dev": "stripe listen --forward-to localhost:3000/api/webhooks/stripe"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@aws-sdk/client-s3": "3.1140.0",
|
|
31
|
+
"@aws-sdk/s3-request-presigner": "3.1140.0",
|
|
32
|
+
"@ejekanshjain/simple-email-validator": "1.0.4",
|
|
33
|
+
"@hookform/resolvers": "5.9.1",
|
|
34
|
+
"@next/third-parties": "16.3.6",
|
|
35
|
+
"@opentelemetry/api": "1.9.1",
|
|
36
|
+
"@opentelemetry/sdk-trace-base": "2.11.0",
|
|
37
|
+
"@paralleldrive/cuid2": "3.3.0",
|
|
38
|
+
"@react-email/render": "2.1.0",
|
|
39
|
+
"@t3-oss/env-nextjs": "0.13.11",
|
|
40
|
+
"@tanstack/react-query": "5.103.2",
|
|
41
|
+
"@tanstack/react-table": "9.2.4",
|
|
42
|
+
"@vercel/otel": "2.1.3",
|
|
43
|
+
"better-auth": "1.7.6",
|
|
44
|
+
"class-variance-authority": "0.7.1",
|
|
45
|
+
"clsx": "2.1.1",
|
|
46
|
+
"cmdk": "1.1.1",
|
|
47
|
+
"drizzle-orm": "0.45.3",
|
|
48
|
+
"lucide-react": "1.48.0",
|
|
49
|
+
"next": "16.3.6",
|
|
50
|
+
"next-safe-action": "8.7.3",
|
|
51
|
+
"next-themes": "0.4.6",
|
|
52
|
+
"nextjs-toploader": "3.9.17",
|
|
53
|
+
"nodemailer": "10.0.10",
|
|
54
|
+
"nuqs": "2.10.1",
|
|
55
|
+
"postgres": "3.4.9",
|
|
56
|
+
"radix-ui": "1.6.7",
|
|
57
|
+
"react": "19.3.0",
|
|
58
|
+
"react-dom": "19.3.0",
|
|
59
|
+
"react-hook-form": "7.88.0",
|
|
60
|
+
"server-only": "0.0.1",
|
|
61
|
+
"sonner": "2.0.8",
|
|
62
|
+
"stripe": "22.6.2",
|
|
63
|
+
"tailwind-merge": "3.7.0",
|
|
64
|
+
"tw-animate-css": "1.4.0",
|
|
65
|
+
"workflow": "4.8.9",
|
|
66
|
+
"zod": "4.6.5"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@electric-sql/pglite": "0.5.8",
|
|
70
|
+
"@next/bundle-analyzer": "16.3.6",
|
|
71
|
+
"@react-email/preview-server": "5.2.10",
|
|
72
|
+
"@tailwindcss/postcss": "4.3.3",
|
|
73
|
+
"@types/bun": "1.4.2",
|
|
74
|
+
"@types/nodemailer": "8.0.2",
|
|
75
|
+
"@types/react": "19.3.0",
|
|
76
|
+
"@types/react-dom": "19.3.0",
|
|
77
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
78
|
+
"babel-plugin-react-compiler": "1.0.0",
|
|
79
|
+
"drizzle-kit": "0.31.11",
|
|
80
|
+
"eslint": "9.39.4",
|
|
81
|
+
"eslint-config-next": "16.3.6",
|
|
82
|
+
"prettier": "3.9.9",
|
|
83
|
+
"prettier-plugin-organize-imports": "4.3.0",
|
|
84
|
+
"prettier-plugin-tailwindcss": "0.8.1",
|
|
85
|
+
"react-email": "6.11.0",
|
|
86
|
+
"tailwindcss": "4.3.3",
|
|
87
|
+
"typescript": "6.0.3"
|
|
88
|
+
},
|
|
89
|
+
"trustedDependencies": [
|
|
90
|
+
"@nestjs/core",
|
|
91
|
+
"@swc/core",
|
|
92
|
+
"cbor-extract",
|
|
93
|
+
"esbuild",
|
|
94
|
+
"sharp",
|
|
95
|
+
"unrs-resolver"
|
|
96
|
+
]
|
|
97
|
+
}
|