create-ej-app 25.0.0 → 26.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.
Files changed (160) hide show
  1. package/cli.js +4 -0
  2. package/cli.ts +5 -0
  3. package/package.json +2 -2
  4. package/template/api/package.json +2 -2
  5. package/template/nextjs/gitignore +1 -0
  6. package/template/nextjs/package.json +18 -18
  7. package/template/saas-starter/.dockerignore +7 -0
  8. package/template/saas-starter/.env.example +25 -0
  9. package/template/saas-starter/.vscode/extensions.json +11 -0
  10. package/template/saas-starter/.vscode/settings.json +3 -0
  11. package/template/saas-starter/Dockerfile +15 -0
  12. package/template/saas-starter/README.md +3 -0
  13. package/template/saas-starter/components.json +25 -0
  14. package/template/saas-starter/drizzle.config.ts +10 -0
  15. package/template/saas-starter/eslint.config.mjs +23 -0
  16. package/template/saas-starter/gitignore +9 -0
  17. package/template/saas-starter/next-env.d.ts +6 -0
  18. package/template/saas-starter/next.config.ts +14 -0
  19. package/template/saas-starter/package.json +92 -0
  20. package/template/saas-starter/postcss.config.mjs +7 -0
  21. package/template/saas-starter/prettier.config.mjs +11 -0
  22. package/template/saas-starter/public/placeholder.svg +1 -0
  23. package/template/saas-starter/src/actions/uploads.ts +28 -0
  24. package/template/saas-starter/src/actions/uploads.validation.ts +14 -0
  25. package/template/saas-starter/src/app/(admin)/actions/users.ts +97 -0
  26. package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +21 -0
  27. package/template/saas-starter/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
  28. package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +40 -0
  29. package/template/saas-starter/src/app/(admin)/admin/layout.tsx +57 -0
  30. package/template/saas-starter/src/app/(admin)/admin/page.tsx +3 -0
  31. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +183 -0
  32. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +129 -0
  33. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +415 -0
  34. package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +15 -0
  35. package/template/saas-starter/src/app/(app)/actions/billing.ts +152 -0
  36. package/template/saas-starter/src/app/(app)/actions/members.ts +163 -0
  37. package/template/saas-starter/src/app/(app)/actions/organization.ts +92 -0
  38. package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +20 -0
  39. package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +34 -0
  40. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +100 -0
  41. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +364 -0
  42. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +155 -0
  43. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +232 -0
  44. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +116 -0
  45. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +169 -0
  46. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +571 -0
  47. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +40 -0
  48. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +69 -0
  49. package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +29 -0
  50. package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +17 -0
  51. package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +27 -0
  52. package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +41 -0
  53. package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +140 -0
  54. package/template/saas-starter/src/app/(app)/app/layout.tsx +52 -0
  55. package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +129 -0
  56. package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +42 -0
  57. package/template/saas-starter/src/app/(app)/app/page.tsx +81 -0
  58. package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +109 -0
  59. package/template/saas-starter/src/app/(auth)/login/page.tsx +87 -0
  60. package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +75 -0
  61. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +88 -0
  62. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +167 -0
  63. package/template/saas-starter/src/app/(marketing)/layout.tsx +14 -0
  64. package/template/saas-starter/src/app/(marketing)/page.tsx +3 -0
  65. package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +3 -0
  66. package/template/saas-starter/src/app/(marketing)/terms/page.tsx +3 -0
  67. package/template/saas-starter/src/app/api/auth/[...all]/route.ts +4 -0
  68. package/template/saas-starter/src/app/api/webhooks/stripe/route.ts +94 -0
  69. package/template/saas-starter/src/app/favicon.ico +0 -0
  70. package/template/saas-starter/src/app/globals.css +240 -0
  71. package/template/saas-starter/src/app/layout.tsx +53 -0
  72. package/template/saas-starter/src/app/not-found.tsx +27 -0
  73. package/template/saas-starter/src/app/proxy.ts +28 -0
  74. package/template/saas-starter/src/app/robots.ts +32 -0
  75. package/template/saas-starter/src/app/sitemap.ts +20 -0
  76. package/template/saas-starter/src/components/dashboard-header.tsx +64 -0
  77. package/template/saas-starter/src/components/data-table/enum.ts +5 -0
  78. package/template/saas-starter/src/components/data-table/index.tsx +960 -0
  79. package/template/saas-starter/src/components/data-table/types.ts +75 -0
  80. package/template/saas-starter/src/components/error.tsx +26 -0
  81. package/template/saas-starter/src/components/file-upload.tsx +215 -0
  82. package/template/saas-starter/src/components/impersonation-banner.tsx +44 -0
  83. package/template/saas-starter/src/components/loader.tsx +25 -0
  84. package/template/saas-starter/src/components/navigation-search.tsx +140 -0
  85. package/template/saas-starter/src/components/navigation-sidebar.tsx +241 -0
  86. package/template/saas-starter/src/components/page-heading.tsx +32 -0
  87. package/template/saas-starter/src/components/react-query-provider.tsx +12 -0
  88. package/template/saas-starter/src/components/screen-size.tsx +40 -0
  89. package/template/saas-starter/src/components/site-footer.tsx +23 -0
  90. package/template/saas-starter/src/components/theme-provider.tsx +67 -0
  91. package/template/saas-starter/src/components/theme-toggle.tsx +22 -0
  92. package/template/saas-starter/src/components/ui/avatar.tsx +112 -0
  93. package/template/saas-starter/src/components/ui/badge.tsx +49 -0
  94. package/template/saas-starter/src/components/ui/button.tsx +67 -0
  95. package/template/saas-starter/src/components/ui/card.tsx +103 -0
  96. package/template/saas-starter/src/components/ui/checkbox.tsx +32 -0
  97. package/template/saas-starter/src/components/ui/collapsible.tsx +33 -0
  98. package/template/saas-starter/src/components/ui/command.tsx +192 -0
  99. package/template/saas-starter/src/components/ui/dialog.tsx +164 -0
  100. package/template/saas-starter/src/components/ui/dropdown-menu.tsx +273 -0
  101. package/template/saas-starter/src/components/ui/field.tsx +238 -0
  102. package/template/saas-starter/src/components/ui/input-group.tsx +156 -0
  103. package/template/saas-starter/src/components/ui/input.tsx +19 -0
  104. package/template/saas-starter/src/components/ui/label.tsx +24 -0
  105. package/template/saas-starter/src/components/ui/popover.tsx +89 -0
  106. package/template/saas-starter/src/components/ui/select.tsx +195 -0
  107. package/template/saas-starter/src/components/ui/separator.tsx +28 -0
  108. package/template/saas-starter/src/components/ui/sheet.tsx +143 -0
  109. package/template/saas-starter/src/components/ui/sidebar.tsx +702 -0
  110. package/template/saas-starter/src/components/ui/skeleton.tsx +13 -0
  111. package/template/saas-starter/src/components/ui/sonner.tsx +45 -0
  112. package/template/saas-starter/src/components/ui/switch.tsx +33 -0
  113. package/template/saas-starter/src/components/ui/table.tsx +116 -0
  114. package/template/saas-starter/src/components/ui/tabs.tsx +90 -0
  115. package/template/saas-starter/src/components/ui/textarea.tsx +18 -0
  116. package/template/saas-starter/src/components/ui/tooltip.tsx +57 -0
  117. package/template/saas-starter/src/components/user-menu.tsx +90 -0
  118. package/template/saas-starter/src/db/common.ts +31 -0
  119. package/template/saas-starter/src/db/enums.ts +26 -0
  120. package/template/saas-starter/src/db/index.ts +51 -0
  121. package/template/saas-starter/src/db/relations.ts +75 -0
  122. package/template/saas-starter/src/db/schema.ts +191 -0
  123. package/template/saas-starter/src/emails/components/email-layout.tsx +63 -0
  124. package/template/saas-starter/src/emails/invitation.tsx +56 -0
  125. package/template/saas-starter/src/emails/magic-link.tsx +49 -0
  126. package/template/saas-starter/src/emails/test.tsx +32 -0
  127. package/template/saas-starter/src/emails/welcome.tsx +34 -0
  128. package/template/saas-starter/src/env.ts +66 -0
  129. package/template/saas-starter/src/hooks/use-mobile.ts +19 -0
  130. package/template/saas-starter/src/instrumentation.ts +26 -0
  131. package/template/saas-starter/src/lib/actions.ts +21 -0
  132. package/template/saas-starter/src/lib/admin-navigation.ts +31 -0
  133. package/template/saas-starter/src/lib/app-navigation.ts +59 -0
  134. package/template/saas-starter/src/lib/auth-client.ts +29 -0
  135. package/template/saas-starter/src/lib/auth-permissions.ts +22 -0
  136. package/template/saas-starter/src/lib/auth.ts +211 -0
  137. package/template/saas-starter/src/lib/billing.ts +117 -0
  138. package/template/saas-starter/src/lib/cn.ts +4 -0
  139. package/template/saas-starter/src/lib/constants.ts +22 -0
  140. package/template/saas-starter/src/lib/delay.ts +2 -0
  141. package/template/saas-starter/src/lib/email-service.ts +89 -0
  142. package/template/saas-starter/src/lib/format-currency.ts +27 -0
  143. package/template/saas-starter/src/lib/format-date.ts +12 -0
  144. package/template/saas-starter/src/lib/nodemailer.ts +43 -0
  145. package/template/saas-starter/src/lib/organization-access.ts +93 -0
  146. package/template/saas-starter/src/lib/organization-plan-limits.ts +38 -0
  147. package/template/saas-starter/src/lib/otel.ts +27 -0
  148. package/template/saas-starter/src/lib/plans.ts +63 -0
  149. package/template/saas-starter/src/lib/safe-action-client.ts +75 -0
  150. package/template/saas-starter/src/lib/safe-action.ts +67 -0
  151. package/template/saas-starter/src/lib/siteConfig.ts +4 -0
  152. package/template/saas-starter/src/lib/storage.ts +181 -0
  153. package/template/saas-starter/src/lib/stripe.ts +6 -0
  154. package/template/saas-starter/src/lib/toast-message.ts +15 -0
  155. package/template/saas-starter/src/lib/validations.ts +12 -0
  156. package/template/saas-starter/src/scripts/reset-db.ts +22 -0
  157. package/template/saas-starter/src/scripts/seed.ts +11 -0
  158. package/template/saas-starter/src/scripts/setup-stripe-plans.ts +200 -0
  159. package/template/saas-starter/src/scripts/test-mail.ts +35 -0
  160. package/template/saas-starter/tsconfig.json +51 -0
package/cli.js CHANGED
@@ -65,6 +65,10 @@ program.action(async () => {
65
65
  name: 'nextjs',
66
66
  message: 'Full Stack setup with Next 16, Better Auth, shadcn and Drizzle'
67
67
  },
68
+ {
69
+ name: 'saas-starter',
70
+ message: 'SaaS Starter with Stripe, Next 16, Better Auth, shadcn and Drizzle'
71
+ },
68
72
  {
69
73
  name: 'api',
70
74
  message: 'API Server setup with Elysia, Drizzle, Typescript'
package/cli.ts CHANGED
@@ -78,6 +78,11 @@ program.action(async () => {
78
78
  message:
79
79
  'Full Stack setup with Next 16, Better Auth, shadcn and Drizzle'
80
80
  },
81
+ {
82
+ name: 'saas-starter',
83
+ message:
84
+ 'SaaS Starter with Stripe, Next 16, Better Auth, shadcn and Drizzle'
85
+ },
81
86
  {
82
87
  name: 'api',
83
88
  message: 'API Server setup with Elysia, Drizzle, Typescript'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ej-app",
3
- "version": "25.0.0",
3
+ "version": "26.0.0",
4
4
  "description": "Bootstrap a next.js app with my preferred setup",
5
5
  "main": "cli.js",
6
6
  "type": "module",
@@ -36,7 +36,7 @@
36
36
  "enquirer": "^2.4.1"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/node": "^25.9.2",
39
+ "@types/node": "^25.9.3",
40
40
  "typescript": "^6.0.3"
41
41
  }
42
42
  }
@@ -22,10 +22,10 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/bun": "1.3.14",
25
- "@typescript/native-preview": "7.0.0-dev.20260607.1",
25
+ "@typescript/native-preview": "7.0.0-dev.20260614.1",
26
26
  "drizzle-kit": "0.31.10",
27
27
  "postgres": "3.4.9",
28
- "prettier": "3.8.3",
28
+ "prettier": "3.8.4",
29
29
  "prettier-plugin-organize-imports": "4.3.0"
30
30
  }
31
31
  }
@@ -3,5 +3,6 @@ node_modules
3
3
  out
4
4
  build
5
5
  next-env.d.ts
6
+ .swc
6
7
  .DS_Store
7
8
  .env
@@ -23,53 +23,53 @@
23
23
  "dependencies": {
24
24
  "@ejekanshjain/simple-email-validator": "1.0.4",
25
25
  "@hookform/resolvers": "5.4.0",
26
- "@next/third-parties": "16.2.7",
26
+ "@next/third-parties": "16.2.9",
27
27
  "@opentelemetry/api": "1.9.1",
28
- "@opentelemetry/sdk-trace-base": "2.7.1",
28
+ "@opentelemetry/sdk-trace-base": "2.8.0",
29
29
  "@paralleldrive/cuid2": "3.3.0",
30
30
  "@react-email/render": "2.0.8",
31
31
  "@t3-oss/env-nextjs": "0.13.11",
32
32
  "@tanstack/react-query": "5.101.0",
33
- "@vercel/otel": "2.1.2",
34
- "better-auth": "1.6.14",
33
+ "@vercel/otel": "2.1.3",
34
+ "better-auth": "1.6.18",
35
35
  "class-variance-authority": "0.7.1",
36
36
  "clsx": "2.1.1",
37
37
  "drizzle-orm": "0.45.2",
38
- "lucide-react": "1.17.0",
39
- "next": "16.2.7",
40
- "next-safe-action": "8.5.3",
38
+ "lucide-react": "1.18.0",
39
+ "next": "16.2.9",
40
+ "next-safe-action": "8.5.4",
41
41
  "next-themes": "^0.4.6",
42
42
  "nextjs-toploader": "3.9.17",
43
- "nodemailer": "8.0.10",
43
+ "nodemailer": "9.0.0",
44
44
  "postgres": "3.4.9",
45
45
  "radix-ui": "1.5.0",
46
46
  "react": "19.2.7",
47
47
  "react-dom": "19.2.7",
48
- "react-hook-form": "7.78.0",
48
+ "react-hook-form": "7.79.0",
49
49
  "sonner": "^2.0.7",
50
50
  "tailwind-merge": "3.6.0",
51
51
  "tw-animate-css": "1.4.0",
52
- "workflow": "4.3.1",
52
+ "workflow": "4.4.0",
53
53
  "zod": "4.4.3"
54
54
  },
55
55
  "devDependencies": {
56
- "@next/bundle-analyzer": "16.2.7",
56
+ "@next/bundle-analyzer": "16.2.9",
57
57
  "@react-email/preview-server": "5.2.10",
58
- "@tailwindcss/postcss": "4.3.0",
58
+ "@tailwindcss/postcss": "4.3.1",
59
59
  "@types/bun": "1.3.14",
60
- "@types/nodemailer": "8.0.0",
60
+ "@types/nodemailer": "8.0.1",
61
61
  "@types/react": "19.2.17",
62
62
  "@types/react-dom": "19.2.3",
63
- "@typescript/native-preview": "7.0.0-dev.20260607.1",
63
+ "@typescript/native-preview": "7.0.0-dev.20260614.1",
64
64
  "babel-plugin-react-compiler": "1.0.0",
65
65
  "drizzle-kit": "0.31.10",
66
66
  "eslint": "9.39.4",
67
- "eslint-config-next": "16.2.7",
68
- "prettier": "3.8.3",
67
+ "eslint-config-next": "16.2.9",
68
+ "prettier": "3.8.4",
69
69
  "prettier-plugin-organize-imports": "4.3.0",
70
70
  "prettier-plugin-tailwindcss": "0.8.0",
71
- "react-email": "6.5.0",
72
- "tailwindcss": "4.3.0",
71
+ "react-email": "6.6.0",
72
+ "tailwindcss": "4.3.1",
73
73
  "typescript": "6.0.3"
74
74
  },
75
75
  "trustedDependencies": [
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ .next
3
+ out
4
+ build
5
+ next-env.d.ts
6
+ .DS_Store
7
+ .env
@@ -0,0 +1,25 @@
1
+ APP_ENV="production"
2
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
3
+ BETTER_AUTH_URL="http://localhost:3000"
4
+ BETTER_AUTH_SECRET="secret"
5
+ BETTER_AUTH_GITHUB_ID="github_id"
6
+ BETTER_AUTH_GITHUB_SECRET="github_secret"
7
+ BETTER_AUTH_GOOGLE_ID="google_id"
8
+ BETTER_AUTH_GOOGLE_SECRET="google_secret"
9
+ EMAIL_SERVER_USER="username"
10
+ EMAIL_SERVER_PASSWORD="password"
11
+ EMAIL_SERVER_HOST="smtp.example.com"
12
+ EMAIL_SERVER_PORT="465"
13
+ EMAIL_FROM="no-reply@example.com"
14
+ STRIPE_SECRET_KEY="sk_test_xxxxxxxxxx"
15
+ STRIPE_WEBHOOK_SECRET="whsec_xxxxxxxxxx"
16
+ STRIPE_PRICE_PRO_MONTHLY="price_xxxxxxxxxx"
17
+ STRIPE_PRICE_PRO_ANNUAL="price_xxxxxxxxxx"
18
+ STRIPE_PRICE_ULTIMATE_MONTHLY="price_xxxxxxxxxx"
19
+ STRIPE_PRICE_ULTIMATE_ANNUAL="price_xxxxxxxxxx"
20
+ STRIPE_PORTAL_CONFIGURATION_ID="bpc_xxxxxxxxxx"
21
+ R2_ACCOUNT_ID=""
22
+ R2_ACCESS_KEY_ID=""
23
+ R2_SECRET_ACCESS_KEY=""
24
+ R2_BUCKET_NAME=""
25
+ R2_PUBLIC_URL=""
@@ -0,0 +1,11 @@
1
+ {
2
+ "recommendations": [
3
+ "bradlc.vscode-tailwindcss",
4
+ "dbaeumer.vscode-eslint",
5
+ "eamodio.gitlens",
6
+ "esbenp.prettier-vscode",
7
+ "TypeScriptTeam.native-preview",
8
+ "usernamehw.errorlens",
9
+ "YoavBls.pretty-ts-errors"
10
+ ]
11
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "js/ts.experimental.useTsgo": true
3
+ }
@@ -0,0 +1,15 @@
1
+ FROM oven/bun:latest
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json bun.lock ./
6
+
7
+ RUN bun install --frozen-lockfile
8
+
9
+ COPY . .
10
+
11
+ COPY .env.example .env
12
+
13
+ RUN bun run build
14
+
15
+ ENTRYPOINT ["bun", "start"]
@@ -0,0 +1,3 @@
1
+ # my-project
2
+
3
+ A Legendary Project
@@ -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,10 @@
1
+ import { defineConfig } from 'drizzle-kit'
2
+ import { env } from '~/env'
3
+
4
+ export default defineConfig({
5
+ schema: './src/db/schema.ts',
6
+ dialect: 'postgresql',
7
+ dbCredentials: {
8
+ url: env.DATABASE_URL
9
+ }
10
+ })
@@ -0,0 +1,23 @@
1
+ // @ts-ignore
2
+ import nextVitals from 'eslint-config-next/core-web-vitals'
3
+ // import nextTs from 'eslint-config-next/typescript'
4
+ import { defineConfig, globalIgnores } from 'eslint/config'
5
+
6
+ const eslintConfig = defineConfig([
7
+ ...nextVitals,
8
+ // ...nextTs,
9
+ globalIgnores([
10
+ 'node_modules/**',
11
+ '.next/**',
12
+ 'out/**',
13
+ 'build/**',
14
+ 'next-env.d.ts'
15
+ ]),
16
+ {
17
+ rules: {
18
+ 'react/no-unescaped-entities': 'off'
19
+ }
20
+ }
21
+ ])
22
+
23
+ export default eslintConfig
@@ -0,0 +1,9 @@
1
+ node_modules
2
+ .next
3
+ out
4
+ build
5
+ next-env.d.ts
6
+ .swc
7
+ .DS_Store
8
+ .env
9
+ stripe.txt
@@ -0,0 +1,6 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/dev/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,14 @@
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
+
6
+ const nextConfig: NextConfig = {
7
+ reactCompiler: true
8
+ }
9
+
10
+ const withBundleAnalyzer = bundleAnalyzer({
11
+ enabled: env.ANALYZE === 'true'
12
+ })
13
+
14
+ export default withWorkflow(withBundleAnalyzer(nextConfig))
@@ -0,0 +1,92 @@
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 ./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 --write \"./src/**/*.{js,cjs,mjs,jsx,ts,tsx}\"",
18
+ "db:push": "drizzle-kit push",
19
+ "db:seed": "bun ./src/scripts/seed.ts",
20
+ "db:reset": "bun ./src/scripts/reset-db.ts",
21
+ "db:studio": "drizzle-kit studio",
22
+ "gen:auth": "bunx @better-auth/cli generate",
23
+ "stripe:setup": "bun ./src/scripts/setup-stripe-plans.ts",
24
+ "stripe:dev": "stripe listen --forward-to localhost:3000/api/webhooks/stripe"
25
+ },
26
+ "dependencies": {
27
+ "@aws-sdk/client-s3": "3.1068.0",
28
+ "@aws-sdk/s3-request-presigner": "3.1068.0",
29
+ "@ejekanshjain/simple-email-validator": "1.0.4",
30
+ "@hookform/resolvers": "5.4.0",
31
+ "@next/third-parties": "16.2.9",
32
+ "@opentelemetry/api": "1.9.1",
33
+ "@opentelemetry/sdk-trace-base": "2.8.0",
34
+ "@paralleldrive/cuid2": "3.3.0",
35
+ "@react-email/render": "2.0.8",
36
+ "@t3-oss/env-nextjs": "0.13.11",
37
+ "@tanstack/react-query": "5.101.0",
38
+ "@tanstack/react-table": "8.21.3",
39
+ "@vercel/otel": "2.1.3",
40
+ "better-auth": "1.6.18",
41
+ "class-variance-authority": "0.7.1",
42
+ "clsx": "2.1.1",
43
+ "cmdk": "1.1.1",
44
+ "drizzle-orm": "0.45.2",
45
+ "lucide-react": "1.18.0",
46
+ "next": "16.2.9",
47
+ "next-safe-action": "8.5.4",
48
+ "next-themes": "0.4.6",
49
+ "nextjs-toploader": "3.9.17",
50
+ "nodemailer": "9.0.0",
51
+ "nuqs": "2.8.9",
52
+ "postgres": "3.4.9",
53
+ "radix-ui": "1.5.0",
54
+ "react": "19.2.7",
55
+ "react-dom": "19.2.7",
56
+ "react-hook-form": "7.79.0",
57
+ "sonner": "2.0.7",
58
+ "stripe": "22.2.1",
59
+ "tailwind-merge": "3.6.0",
60
+ "tw-animate-css": "1.4.0",
61
+ "workflow": "4.4.0",
62
+ "zod": "4.4.3"
63
+ },
64
+ "devDependencies": {
65
+ "@next/bundle-analyzer": "16.2.9",
66
+ "@react-email/preview-server": "5.2.10",
67
+ "@tailwindcss/postcss": "4.3.1",
68
+ "@types/bun": "1.3.14",
69
+ "@types/nodemailer": "8.0.1",
70
+ "@types/react": "19.2.17",
71
+ "@types/react-dom": "19.2.3",
72
+ "@typescript/native-preview": "7.0.0-dev.20260614.1",
73
+ "babel-plugin-react-compiler": "1.0.0",
74
+ "drizzle-kit": "0.31.10",
75
+ "eslint": "9.39.4",
76
+ "eslint-config-next": "16.2.9",
77
+ "prettier": "3.8.4",
78
+ "prettier-plugin-organize-imports": "4.3.0",
79
+ "prettier-plugin-tailwindcss": "0.8.0",
80
+ "react-email": "6.6.0",
81
+ "tailwindcss": "4.3.1",
82
+ "typescript": "6.0.3"
83
+ },
84
+ "trustedDependencies": [
85
+ "@nestjs/core",
86
+ "@swc/core",
87
+ "cbor-extract",
88
+ "esbuild",
89
+ "sharp",
90
+ "unrs-resolver"
91
+ ]
92
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {}
4
+ }
5
+ }
6
+
7
+ export default config
@@ -0,0 +1,11 @@
1
+ /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
2
+ const config = {
3
+ plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'],
4
+ arrowParens: 'avoid',
5
+ trailingComma: 'none',
6
+ semi: false,
7
+ singleQuote: true,
8
+ tabWidth: 2
9
+ }
10
+
11
+ export default config
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
@@ -0,0 +1,28 @@
1
+ 'use server'
2
+
3
+ import { assertUserMembership } from '~/lib/organization-access'
4
+ import { authActionClient } from '~/lib/safe-action'
5
+ import { generateUploadUrl, isR2Configured } from '~/lib/storage'
6
+ import { generateUploadUrlSchema } from './uploads.validation'
7
+
8
+ export const generateUploadUrlAction = authActionClient
9
+ .inputSchema(generateUploadUrlSchema)
10
+ .action(async ({ parsedInput, ctx: { user } }) => {
11
+ if (!isR2Configured()) {
12
+ return { mode: 'base64' as const }
13
+ }
14
+
15
+ if (parsedInput.organizationId) {
16
+ await assertUserMembership(parsedInput.organizationId)
17
+ }
18
+
19
+ const { uploadUrl, key } = await generateUploadUrl({
20
+ filename: parsedInput.filename,
21
+ mimeType: parsedInput.mimeType,
22
+ size: parsedInput.size,
23
+ uploadedBy: user.id,
24
+ organizationId: parsedInput.organizationId
25
+ })
26
+
27
+ return { mode: 'r2' as const, uploadUrl, key }
28
+ })
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod'
2
+ import { MAX_FILE_SIZE_BYTES, MAX_FILE_SIZE_MB } from '~/lib/constants'
3
+ import { stringValidation } from '~/lib/validations'
4
+
5
+ export const generateUploadUrlSchema = z.object({
6
+ filename: stringValidation,
7
+ mimeType: stringValidation,
8
+ size: z
9
+ .number()
10
+ .int()
11
+ .positive()
12
+ .max(MAX_FILE_SIZE_BYTES, `File must be under ${MAX_FILE_SIZE_MB}MB`),
13
+ organizationId: stringValidation
14
+ })
@@ -0,0 +1,97 @@
1
+ 'use server'
2
+
3
+ import {
4
+ and,
5
+ asc,
6
+ count,
7
+ desc,
8
+ ilike,
9
+ inArray,
10
+ isNotNull,
11
+ or
12
+ } from 'drizzle-orm'
13
+ import { z } from 'zod'
14
+ import { SortOrderEnum } from '~/components/data-table/enum'
15
+ import { db } from '~/db'
16
+ import { usersTable } from '~/db/schema'
17
+ import { getPaginationSchema, getSortSchema } from '~/lib/actions'
18
+ import { adminActionClient } from '~/lib/safe-action'
19
+
20
+ export const getUsers = adminActionClient
21
+ .inputSchema(
22
+ z.object({
23
+ ...getPaginationSchema(),
24
+ ...getSortSchema(['name', 'email', 'createdAt']),
25
+ search: z.string().trim().optional(),
26
+ filters: z
27
+ .object({
28
+ role: z.array(z.string()).optional(),
29
+ banned: z.array(z.boolean()).optional()
30
+ })
31
+ .optional()
32
+ })
33
+ )
34
+ .action(
35
+ async ({
36
+ parsedInput: { page, limit, sortBy, sortOrder, search, filters }
37
+ }) => {
38
+ const where = and(
39
+ search
40
+ ? or(
41
+ ilike(usersTable.name, `%${search}%`),
42
+ ilike(usersTable.email, `%${search}%`)
43
+ )
44
+ : undefined,
45
+ filters?.role && filters.role.length > 0
46
+ ? inArray(usersTable.role, filters.role)
47
+ : undefined,
48
+ filters?.banned && filters.banned.length > 0
49
+ ? inArray(usersTable.banned, filters.banned)
50
+ : undefined
51
+ )
52
+
53
+ const [results, total] = await Promise.all([
54
+ db.query.usersTable.findMany({
55
+ where,
56
+ limit,
57
+ offset: (page - 1) * limit,
58
+ orderBy: [
59
+ sortOrder === SortOrderEnum.ASC
60
+ ? asc(usersTable[sortBy || 'createdAt'])
61
+ : desc(usersTable[sortBy || 'createdAt'])
62
+ ]
63
+ }),
64
+ db.select({ count: count() }).from(usersTable).where(where)
65
+ ])
66
+
67
+ return [results, total[0]?.count ?? 0] as const
68
+ }
69
+ )
70
+
71
+ export const getDistinctRoles = adminActionClient
72
+ .inputSchema(
73
+ z.object({
74
+ search: z.string().trim().optional(),
75
+ ...getPaginationSchema(20)
76
+ })
77
+ )
78
+ .action(async ({ parsedInput: { search, limit } }) => {
79
+ const where = and(
80
+ isNotNull(usersTable.role),
81
+ search ? ilike(usersTable.role, `%${search}%`) : undefined
82
+ )
83
+
84
+ const rows = await db
85
+ .selectDistinct({ role: usersTable.role })
86
+ .from(usersTable)
87
+ .where(where)
88
+ .orderBy(asc(usersTable.role))
89
+ .limit(limit)
90
+
91
+ return rows
92
+ .filter(r => r.role !== null)
93
+ .map(r => ({
94
+ value: r.role!,
95
+ label: r.role!.charAt(0).toUpperCase() + r.role!.slice(1)
96
+ }))
97
+ })
@@ -0,0 +1,21 @@
1
+ 'use client'
2
+
3
+ import { FC } from 'react'
4
+ import { DashboardHeader } from '~/components/dashboard-header'
5
+ import { AdminSearch } from './admin-search'
6
+
7
+ export const AdminHeader: FC<{
8
+ user: {
9
+ name?: string | null
10
+ email?: string | null
11
+ image?: string | null
12
+ }
13
+ isSuperAdmin: boolean
14
+ }> = ({ user, isSuperAdmin }) => {
15
+ return (
16
+ <DashboardHeader
17
+ user={user}
18
+ search={<AdminSearch isSuperAdmin={isSuperAdmin} />}
19
+ />
20
+ )
21
+ }
@@ -0,0 +1,16 @@
1
+ 'use client'
2
+
3
+ import { FC } from 'react'
4
+ import { NavigationSearch } from '~/components/navigation-search'
5
+ import { getAdminNavigation } from '~/lib/admin-navigation'
6
+
7
+ export const AdminSearch: FC<{ isSuperAdmin: boolean }> = ({
8
+ isSuperAdmin
9
+ }) => {
10
+ return (
11
+ <NavigationSearch
12
+ groups={getAdminNavigation(isSuperAdmin)}
13
+ dialogDescription="Search across all admin pages and settings."
14
+ />
15
+ )
16
+ }
@@ -0,0 +1,40 @@
1
+ 'use client'
2
+
3
+ import { LayoutGrid } from 'lucide-react'
4
+ import Link from 'next/link'
5
+ import { FC } from 'react'
6
+ import { NavigationSidebar } from '~/components/navigation-sidebar'
7
+ import {
8
+ SidebarMenu,
9
+ SidebarMenuButton,
10
+ SidebarMenuItem
11
+ } from '~/components/ui/sidebar'
12
+ import { getAdminNavigation } from '~/lib/admin-navigation'
13
+
14
+ export const AdminSidebar: FC<{
15
+ companyName: string
16
+ isSuperAdmin: boolean
17
+ }> = ({ companyName, isSuperAdmin }) => {
18
+ return (
19
+ <NavigationSidebar
20
+ groups={getAdminNavigation(isSuperAdmin)}
21
+ header={
22
+ <SidebarMenu>
23
+ <SidebarMenuItem>
24
+ <SidebarMenuButton size="lg" asChild>
25
+ <Link href="/admin">
26
+ <div className="bg-primary text-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg">
27
+ <LayoutGrid className="size-4" />
28
+ </div>
29
+ <div className="flex min-w-0 flex-col gap-0.5 leading-none group-data-[collapsible=icon]:hidden">
30
+ <span className="truncate font-semibold">Admin Panel</span>
31
+ <span className="truncate text-xs">{companyName}</span>
32
+ </div>
33
+ </Link>
34
+ </SidebarMenuButton>
35
+ </SidebarMenuItem>
36
+ </SidebarMenu>
37
+ }
38
+ />
39
+ )
40
+ }