vibecarbon 0.1.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 (495) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +188 -0
  3. package/carbon/.claude/hooks/task-completed-gate.sh +19 -0
  4. package/carbon/.claude/hooks/teammate-idle-gate.sh +29 -0
  5. package/carbon/.claude/settings.json +29 -0
  6. package/carbon/.cursor/rules/vibecarbon.mdc +6 -0
  7. package/carbon/.dockerignore +57 -0
  8. package/carbon/.env.example +219 -0
  9. package/carbon/.github/copilot-instructions.md +3 -0
  10. package/carbon/.github/workflows/deploy.yml +346 -0
  11. package/carbon/.github/workflows/vibecarbon-build.yml +81 -0
  12. package/carbon/.windsurfrules +74 -0
  13. package/carbon/AGENTS.md +422 -0
  14. package/carbon/CLAUDE.md +3 -0
  15. package/carbon/DEVELOPMENT.md +187 -0
  16. package/carbon/Dockerfile +98 -0
  17. package/carbon/LICENSE +21 -0
  18. package/carbon/PRODUCTION.md +364 -0
  19. package/carbon/README.md +193 -0
  20. package/carbon/TESTING.md +138 -0
  21. package/carbon/backup/Dockerfile +75 -0
  22. package/carbon/backup/backup.sh +95 -0
  23. package/carbon/backup/compose-backup.sh +140 -0
  24. package/carbon/biome.json +83 -0
  25. package/carbon/cloud-init/docker-ce-setup.yaml +55 -0
  26. package/carbon/cloud-init/k3s/master-init.sh +215 -0
  27. package/carbon/cloud-init/k3s/supabase-init.sh +167 -0
  28. package/carbon/cloud-init/k3s/worker-init.sh +147 -0
  29. package/carbon/components.json +24 -0
  30. package/carbon/content/blog/authentication-guide.mdx +34 -0
  31. package/carbon/content/blog/getting-started.mdx +41 -0
  32. package/carbon/content/changelog/v0-1-0.mdx +40 -0
  33. package/carbon/content/docs/analytics.mdx +90 -0
  34. package/carbon/content/docs/authentication.mdx +231 -0
  35. package/carbon/content/docs/background-jobs.mdx +116 -0
  36. package/carbon/content/docs/cli.mdx +630 -0
  37. package/carbon/content/docs/database.mdx +236 -0
  38. package/carbon/content/docs/deployment.mdx +227 -0
  39. package/carbon/content/docs/development.mdx +238 -0
  40. package/carbon/content/docs/environments.mdx +84 -0
  41. package/carbon/content/docs/getting-started.mdx +112 -0
  42. package/carbon/content/docs/legal/privacy-policy.mdx +89 -0
  43. package/carbon/content/docs/legal/terms-of-service.mdx +99 -0
  44. package/carbon/content/docs/optional-services.mdx +160 -0
  45. package/carbon/db/Dockerfile +23 -0
  46. package/carbon/docker-compose.dev-init.yml +5 -0
  47. package/carbon/docker-compose.metabase.override.yml +14 -0
  48. package/carbon/docker-compose.metabase.prod.yml +28 -0
  49. package/carbon/docker-compose.metabase.yml +84 -0
  50. package/carbon/docker-compose.n8n.override.yml +14 -0
  51. package/carbon/docker-compose.n8n.prod.yml +31 -0
  52. package/carbon/docker-compose.n8n.yml +97 -0
  53. package/carbon/docker-compose.observability.override.yml +18 -0
  54. package/carbon/docker-compose.observability.prod.yml +28 -0
  55. package/carbon/docker-compose.observability.yml +125 -0
  56. package/carbon/docker-compose.override.yml +28 -0
  57. package/carbon/docker-compose.prod.yml +294 -0
  58. package/carbon/docker-compose.yml +508 -0
  59. package/carbon/docker-entrypoint.sh +12 -0
  60. package/carbon/ha/activate-standby.sh +52 -0
  61. package/carbon/ha/primary-init.sql +36 -0
  62. package/carbon/ha/standby-init.sh +74 -0
  63. package/carbon/k8s/LICENSE +99 -0
  64. package/carbon/k8s/README.md +272 -0
  65. package/carbon/k8s/base/app/deployment.yaml +130 -0
  66. package/carbon/k8s/base/app/hpa.yaml +44 -0
  67. package/carbon/k8s/base/app/kustomization.yaml +10 -0
  68. package/carbon/k8s/base/app/network-policy.yaml +124 -0
  69. package/carbon/k8s/base/app/pdb.yaml +14 -0
  70. package/carbon/k8s/base/app/rbac.yaml +36 -0
  71. package/carbon/k8s/base/app/service.yaml +16 -0
  72. package/carbon/k8s/base/backup/cronjob.yaml +120 -0
  73. package/carbon/k8s/base/backup/kustomization.yaml +7 -0
  74. package/carbon/k8s/base/backup/network-policy.yaml +31 -0
  75. package/carbon/k8s/base/backup/rbac.yaml +7 -0
  76. package/carbon/k8s/base/cluster-autoscaler/deployment.yaml +103 -0
  77. package/carbon/k8s/base/cluster-autoscaler/kustomization.yaml +17 -0
  78. package/carbon/k8s/base/cluster-autoscaler/rbac.yaml +109 -0
  79. package/carbon/k8s/base/config/configmap.yaml +48 -0
  80. package/carbon/k8s/base/config/kustomization.yaml +8 -0
  81. package/carbon/k8s/base/hetzner-ccm/kustomization.yaml +9 -0
  82. package/carbon/k8s/base/hetzner-csi/kustomization.yaml +9 -0
  83. package/carbon/k8s/base/kustomization.yaml +43 -0
  84. package/carbon/k8s/base/namespace.yaml +7 -0
  85. package/carbon/k8s/base/network-policies.yaml +95 -0
  86. package/carbon/k8s/base/registry/kustomization.yaml +5 -0
  87. package/carbon/k8s/base/registry/local-registry.yaml +193 -0
  88. package/carbon/k8s/base/traefik/certificate.yaml +21 -0
  89. package/carbon/k8s/base/traefik/configmap.yaml +13 -0
  90. package/carbon/k8s/base/traefik/deployment.yaml +165 -0
  91. package/carbon/k8s/base/traefik/ingressroute.yaml +141 -0
  92. package/carbon/k8s/base/traefik/kustomization.yaml +11 -0
  93. package/carbon/k8s/base/traefik/middleware.yaml +109 -0
  94. package/carbon/k8s/base/traefik/network-policy.yaml +92 -0
  95. package/carbon/k8s/base/traefik/service.yaml +38 -0
  96. package/carbon/k8s/flux/README.md +49 -0
  97. package/carbon/k8s/flux/clusters/primary/vibecarbon.yaml +128 -0
  98. package/carbon/k8s/flux/clusters/standby/vibecarbon.yaml +83 -0
  99. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-release.yaml +38 -0
  100. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-repository.yaml +16 -0
  101. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/kustomization.yaml +10 -0
  102. package/carbon/k8s/gitops/supabase/helm-release.yaml +66 -0
  103. package/carbon/k8s/gitops/supabase/helm-repository.yaml +18 -0
  104. package/carbon/k8s/gitops/supabase/kustomization.yaml +33 -0
  105. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-cloudflare.yaml +51 -0
  106. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-hetzner.yaml +59 -0
  107. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-manual.yaml +43 -0
  108. package/carbon/k8s/infra/cert-manager-resources/kustomization.yaml +15 -0
  109. package/carbon/k8s/infra/kustomization.yaml +21 -0
  110. package/carbon/k8s/infra/traefik-crds/kustomization.yaml +10 -0
  111. package/carbon/k8s/overlays/local/configmap.yaml +23 -0
  112. package/carbon/k8s/overlays/local/ingressroute-studio.yaml +104 -0
  113. package/carbon/k8s/overlays/local/kustomization.yaml +215 -0
  114. package/carbon/k8s/overlays/local/namespace.yaml +8 -0
  115. package/carbon/k8s/overlays/local/secrets.yaml +32 -0
  116. package/carbon/k8s/overlays/production/kustomization.yaml +34 -0
  117. package/carbon/k8s/test-local.sh +318 -0
  118. package/carbon/k8s/values/supabase.values.yaml +133 -0
  119. package/carbon/package.json +154 -0
  120. package/carbon/runtime.Dockerfile +17 -0
  121. package/carbon/scripts/_dev-jwt.mjs +45 -0
  122. package/carbon/scripts/check-rls.ts +53 -0
  123. package/carbon/scripts/dev-init.js +191 -0
  124. package/carbon/scripts/dev.js +191 -0
  125. package/carbon/scripts/docker-down.js +63 -0
  126. package/carbon/scripts/docker-logs.js +59 -0
  127. package/carbon/scripts/docker-up.js +222 -0
  128. package/carbon/scripts/generate-dev-configs.sh +131 -0
  129. package/carbon/scripts/generate-rss.ts +116 -0
  130. package/carbon/scripts/generate-sitemap.ts +102 -0
  131. package/carbon/scripts/k8s-apply.js +71 -0
  132. package/carbon/scripts/k8s-delete.js +75 -0
  133. package/carbon/scripts/lib/manifest.js +176 -0
  134. package/carbon/scripts/secret-scan.mjs +278 -0
  135. package/carbon/scripts/validate-dev-configs.sh +101 -0
  136. package/carbon/src/client/App.tsx +202 -0
  137. package/carbon/src/client/assets/hyperformant-dark.svg +29 -0
  138. package/carbon/src/client/assets/hyperformant-light.svg +29 -0
  139. package/carbon/src/client/assets/logos/aider.svg +1 -0
  140. package/carbon/src/client/assets/logos/antigravity.svg +1 -0
  141. package/carbon/src/client/assets/logos/bolt.svg +1 -0
  142. package/carbon/src/client/assets/logos/claude-code.svg +1 -0
  143. package/carbon/src/client/assets/logos/copilot.svg +1 -0
  144. package/carbon/src/client/assets/logos/cursor.svg +1 -0
  145. package/carbon/src/client/assets/logos/gemini-cli.svg +1 -0
  146. package/carbon/src/client/assets/logos/lovable.svg +1 -0
  147. package/carbon/src/client/assets/logos/openai-codex.svg +1 -0
  148. package/carbon/src/client/assets/logos/v0.svg +1 -0
  149. package/carbon/src/client/assets/logos/windsurf.svg +1 -0
  150. package/carbon/src/client/assets/vibecarbon-icon.svg +19 -0
  151. package/carbon/src/client/assets/vibecarbon-logo-dark.svg +29 -0
  152. package/carbon/src/client/assets/vibecarbon-logo-light.svg +29 -0
  153. package/carbon/src/client/components/AIIntegrationSection.tsx +120 -0
  154. package/carbon/src/client/components/AppSidebar.tsx +760 -0
  155. package/carbon/src/client/components/ArchitectureSection.tsx +46 -0
  156. package/carbon/src/client/components/CTAFooter.tsx +59 -0
  157. package/carbon/src/client/components/ComparisonSection.tsx +132 -0
  158. package/carbon/src/client/components/ContentPanel.tsx +66 -0
  159. package/carbon/src/client/components/ContentSkeleton.tsx +21 -0
  160. package/carbon/src/client/components/ErrorBoundary.tsx +46 -0
  161. package/carbon/src/client/components/FAQSection.tsx +76 -0
  162. package/carbon/src/client/components/FileUpload.tsx +210 -0
  163. package/carbon/src/client/components/HeaderActions.tsx +17 -0
  164. package/carbon/src/client/components/Hero.tsx +608 -0
  165. package/carbon/src/client/components/ImpersonationBanner.tsx +31 -0
  166. package/carbon/src/client/components/LanguageSwitcher.tsx +67 -0
  167. package/carbon/src/client/components/Logo.tsx +87 -0
  168. package/carbon/src/client/components/LogoStrip.tsx +76 -0
  169. package/carbon/src/client/components/MetricsStrip.tsx +48 -0
  170. package/carbon/src/client/components/Nav.tsx +195 -0
  171. package/carbon/src/client/components/NewsletterSignup.tsx +147 -0
  172. package/carbon/src/client/components/NotificationDrawer.tsx +171 -0
  173. package/carbon/src/client/components/PageHeader.tsx +24 -0
  174. package/carbon/src/client/components/PlanGate.tsx +36 -0
  175. package/carbon/src/client/components/PricingSection.tsx +371 -0
  176. package/carbon/src/client/components/SEO.tsx +34 -0
  177. package/carbon/src/client/components/SmoothScroll.tsx +45 -0
  178. package/carbon/src/client/components/TechStackSection.tsx +165 -0
  179. package/carbon/src/client/components/WorkflowSection.tsx +604 -0
  180. package/carbon/src/client/components/admin/DockerLogs.tsx +276 -0
  181. package/carbon/src/client/components/admin/PerformanceMetrics.tsx +230 -0
  182. package/carbon/src/client/components/admin/ServiceCard.tsx +45 -0
  183. package/carbon/src/client/components/admin/ServicesStatus.tsx +296 -0
  184. package/carbon/src/client/components/architecture/ArchitectureDiagram.tsx +413 -0
  185. package/carbon/src/client/components/auth/AuthProvider.tsx +466 -0
  186. package/carbon/src/client/components/effects/FilmGrainOverlay.tsx +35 -0
  187. package/carbon/src/client/components/effects/FresnelEdge.tsx +49 -0
  188. package/carbon/src/client/components/effects/GlowTracker.tsx +46 -0
  189. package/carbon/src/client/components/effects/SparkBurst.tsx +145 -0
  190. package/carbon/src/client/components/effects/VGlowEffect.tsx +83 -0
  191. package/carbon/src/client/components/effects/index.ts +2 -0
  192. package/carbon/src/client/components/layouts/SidebarLayout.tsx +20 -0
  193. package/carbon/src/client/components/scroll/ScrollSection.tsx +76 -0
  194. package/carbon/src/client/components/scroll/ScrollytellingProvider.tsx +81 -0
  195. package/carbon/src/client/components/scroll/index.ts +2 -0
  196. package/carbon/src/client/components/ui/accordion.tsx +71 -0
  197. package/carbon/src/client/components/ui/alert-dialog.tsx +162 -0
  198. package/carbon/src/client/components/ui/alert.tsx +73 -0
  199. package/carbon/src/client/components/ui/aspect-ratio.tsx +22 -0
  200. package/carbon/src/client/components/ui/avatar.tsx +91 -0
  201. package/carbon/src/client/components/ui/badge.tsx +50 -0
  202. package/carbon/src/client/components/ui/breadcrumb.tsx +100 -0
  203. package/carbon/src/client/components/ui/button-group.tsx +78 -0
  204. package/carbon/src/client/components/ui/button.tsx +120 -0
  205. package/carbon/src/client/components/ui/calendar.tsx +182 -0
  206. package/carbon/src/client/components/ui/card.tsx +85 -0
  207. package/carbon/src/client/components/ui/carousel.tsx +227 -0
  208. package/carbon/src/client/components/ui/chart.tsx +357 -0
  209. package/carbon/src/client/components/ui/checkbox.tsx +27 -0
  210. package/carbon/src/client/components/ui/collapsible.tsx +15 -0
  211. package/carbon/src/client/components/ui/command.tsx +178 -0
  212. package/carbon/src/client/components/ui/context-menu.tsx +233 -0
  213. package/carbon/src/client/components/ui/dialog.tsx +132 -0
  214. package/carbon/src/client/components/ui/drawer.tsx +118 -0
  215. package/carbon/src/client/components/ui/dropdown-menu.tsx +242 -0
  216. package/carbon/src/client/components/ui/empty.tsx +94 -0
  217. package/carbon/src/client/components/ui/field.tsx +226 -0
  218. package/carbon/src/client/components/ui/hover-card.tsx +44 -0
  219. package/carbon/src/client/components/ui/input-group.tsx +146 -0
  220. package/carbon/src/client/components/ui/input-otp.tsx +83 -0
  221. package/carbon/src/client/components/ui/input.tsx +20 -0
  222. package/carbon/src/client/components/ui/item.tsx +188 -0
  223. package/carbon/src/client/components/ui/kbd.tsx +26 -0
  224. package/carbon/src/client/components/ui/label.tsx +21 -0
  225. package/carbon/src/client/components/ui/menubar.tsx +250 -0
  226. package/carbon/src/client/components/ui/navigation-menu.tsx +155 -0
  227. package/carbon/src/client/components/ui/pagination.tsx +102 -0
  228. package/carbon/src/client/components/ui/popover.tsx +75 -0
  229. package/carbon/src/client/components/ui/progress.tsx +66 -0
  230. package/carbon/src/client/components/ui/radio-group.tsx +36 -0
  231. package/carbon/src/client/components/ui/resizable.tsx +46 -0
  232. package/carbon/src/client/components/ui/scroll-area.tsx +48 -0
  233. package/carbon/src/client/components/ui/select.tsx +186 -0
  234. package/carbon/src/client/components/ui/separator.tsx +21 -0
  235. package/carbon/src/client/components/ui/sheet.tsx +124 -0
  236. package/carbon/src/client/components/ui/sidebar.tsx +702 -0
  237. package/carbon/src/client/components/ui/skeleton.tsx +13 -0
  238. package/carbon/src/client/components/ui/slider.tsx +57 -0
  239. package/carbon/src/client/components/ui/sonner.tsx +45 -0
  240. package/carbon/src/client/components/ui/spinner.tsx +15 -0
  241. package/carbon/src/client/components/ui/switch.tsx +30 -0
  242. package/carbon/src/client/components/ui/table.tsx +89 -0
  243. package/carbon/src/client/components/ui/tabs.tsx +73 -0
  244. package/carbon/src/client/components/ui/textarea.tsx +18 -0
  245. package/carbon/src/client/components/ui/toggle-group.tsx +87 -0
  246. package/carbon/src/client/components/ui/toggle.tsx +44 -0
  247. package/carbon/src/client/components/ui/tooltip.tsx +56 -0
  248. package/carbon/src/client/hooks/api/index.ts +14 -0
  249. package/carbon/src/client/hooks/api/useAuthSettings.ts +80 -0
  250. package/carbon/src/client/hooks/api/useSubscription.ts +87 -0
  251. package/carbon/src/client/hooks/use-mobile.ts +21 -0
  252. package/carbon/src/client/hooks/useMousePosition.ts +91 -0
  253. package/carbon/src/client/hooks/useNotifications.tsx +124 -0
  254. package/carbon/src/client/hooks/useOrganizationMembers.ts +127 -0
  255. package/carbon/src/client/hooks/useOrganizations.tsx +230 -0
  256. package/carbon/src/client/hooks/usePackageManager.ts +69 -0
  257. package/carbon/src/client/hooks/useReducedMotion.ts +20 -0
  258. package/carbon/src/client/hooks/useRunningServices.ts +114 -0
  259. package/carbon/src/client/hooks/useScrollProgress.ts +56 -0
  260. package/carbon/src/client/index.css +467 -0
  261. package/carbon/src/client/index.html +56 -0
  262. package/carbon/src/client/lib/admin-services.ts +151 -0
  263. package/carbon/src/client/lib/api.ts +32 -0
  264. package/carbon/src/client/lib/blog.ts +35 -0
  265. package/carbon/src/client/lib/changelog.ts +33 -0
  266. package/carbon/src/client/lib/docs-search.ts +101 -0
  267. package/carbon/src/client/lib/docs.ts +37 -0
  268. package/carbon/src/client/lib/i18n.ts +32 -0
  269. package/carbon/src/client/lib/supabase.ts +72 -0
  270. package/carbon/src/client/lib/tailwind-colors.ts +357 -0
  271. package/carbon/src/client/lib/theme.ts +117 -0
  272. package/carbon/src/client/lib/utils.ts +22 -0
  273. package/carbon/src/client/locales/de.json +529 -0
  274. package/carbon/src/client/locales/en.json +461 -0
  275. package/carbon/src/client/locales/es.json +529 -0
  276. package/carbon/src/client/locales/fr.json +529 -0
  277. package/carbon/src/client/locales/pt.json +529 -0
  278. package/carbon/src/client/main.tsx +56 -0
  279. package/carbon/src/client/mdx.d.ts +13 -0
  280. package/carbon/src/client/pages/ApiDocs.tsx +76 -0
  281. package/carbon/src/client/pages/AuthCallback.tsx +34 -0
  282. package/carbon/src/client/pages/Blog.tsx +167 -0
  283. package/carbon/src/client/pages/Changelog.tsx +171 -0
  284. package/carbon/src/client/pages/Charts.tsx +388 -0
  285. package/carbon/src/client/pages/Checkout.tsx +227 -0
  286. package/carbon/src/client/pages/Contact.tsx +174 -0
  287. package/carbon/src/client/pages/Dashboard.tsx +368 -0
  288. package/carbon/src/client/pages/Docs.tsx +372 -0
  289. package/carbon/src/client/pages/ForgotPassword.tsx +111 -0
  290. package/carbon/src/client/pages/Home.tsx +187 -0
  291. package/carbon/src/client/pages/Legal.tsx +100 -0
  292. package/carbon/src/client/pages/Login.tsx +408 -0
  293. package/carbon/src/client/pages/MFAVerify.tsx +156 -0
  294. package/carbon/src/client/pages/NotFound.tsx +21 -0
  295. package/carbon/src/client/pages/Onboarding.tsx +246 -0
  296. package/carbon/src/client/pages/ResetPassword.tsx +200 -0
  297. package/carbon/src/client/pages/UIComponents.tsx +390 -0
  298. package/carbon/src/client/pages/admin/ContactSubmissions.tsx +220 -0
  299. package/carbon/src/client/pages/admin/Dashboard.tsx +24 -0
  300. package/carbon/src/client/pages/admin/Infrastructure.tsx +257 -0
  301. package/carbon/src/client/pages/admin/Jobs.tsx +225 -0
  302. package/carbon/src/client/pages/admin/Logs.tsx +18 -0
  303. package/carbon/src/client/pages/admin/Newsletter.tsx +300 -0
  304. package/carbon/src/client/pages/admin/Notifications.tsx +603 -0
  305. package/carbon/src/client/pages/admin/Organizations.tsx +306 -0
  306. package/carbon/src/client/pages/admin/Settings.tsx +314 -0
  307. package/carbon/src/client/pages/admin/Theme.tsx +465 -0
  308. package/carbon/src/client/pages/admin/Users.tsx +365 -0
  309. package/carbon/src/client/pages/api-docs.css +156 -0
  310. package/carbon/src/client/pages/organizations/Details.tsx +200 -0
  311. package/carbon/src/client/pages/organizations/Members.tsx +402 -0
  312. package/carbon/src/client/pages/settings/Billing.tsx +473 -0
  313. package/carbon/src/client/pages/settings/Profile.tsx +160 -0
  314. package/carbon/src/client/pages/settings/Security.tsx +341 -0
  315. package/carbon/src/client/public/favicon.svg +19 -0
  316. package/carbon/src/client/public/robots.txt +8 -0
  317. package/carbon/src/server/billing/index.ts +104 -0
  318. package/carbon/src/server/billing/provider.ts +81 -0
  319. package/carbon/src/server/billing/providers/paddle.ts +314 -0
  320. package/carbon/src/server/billing/providers/polar.ts +325 -0
  321. package/carbon/src/server/billing/providers/stripe.ts +233 -0
  322. package/carbon/src/server/emails/templates.ts +116 -0
  323. package/carbon/src/server/index.ts +554 -0
  324. package/carbon/src/server/lib/auth.ts +6 -0
  325. package/carbon/src/server/lib/email.ts +64 -0
  326. package/carbon/src/server/lib/env.ts +112 -0
  327. package/carbon/src/server/lib/errors.ts +21 -0
  328. package/carbon/src/server/lib/logger.ts +17 -0
  329. package/carbon/src/server/lib/rate-limiter.ts +288 -0
  330. package/carbon/src/server/lib/request.ts +34 -0
  331. package/carbon/src/server/lib/stripe.ts +42 -0
  332. package/carbon/src/server/lib/supabase.ts +65 -0
  333. package/carbon/src/server/middleware/requirePlan.ts +80 -0
  334. package/carbon/src/server/routes/_internal/services-status.ts +958 -0
  335. package/carbon/src/server/routes/_internal/verify-role.ts +185 -0
  336. package/carbon/src/server/routes/health.ts +48 -0
  337. package/carbon/src/server/routes/v1/admin/contact.ts +128 -0
  338. package/carbon/src/server/routes/v1/admin/jobs.ts +171 -0
  339. package/carbon/src/server/routes/v1/admin/newsletter.ts +237 -0
  340. package/carbon/src/server/routes/v1/auth.ts +390 -0
  341. package/carbon/src/server/routes/v1/billing.ts +718 -0
  342. package/carbon/src/server/routes/v1/contact.ts +93 -0
  343. package/carbon/src/server/routes/v1/index.ts +1333 -0
  344. package/carbon/src/server/routes/v1/newsletter.ts +181 -0
  345. package/carbon/src/server/routes/v1/performance.ts +157 -0
  346. package/carbon/src/server/routes/v1/stats.ts +170 -0
  347. package/carbon/src/server/routes/v1/theme.ts +106 -0
  348. package/carbon/src/server/routes/webhooks/billing.ts +376 -0
  349. package/carbon/src/server/routes/webhooks/stripe.ts +276 -0
  350. package/carbon/src/server/types.ts +11 -0
  351. package/carbon/src/shared/pricing.ts +155 -0
  352. package/carbon/src/shared/types.ts +338 -0
  353. package/carbon/supabase/migrations/00001_init.sql +717 -0
  354. package/carbon/supabase/migrations/00002_theme_settings.sql +13 -0
  355. package/carbon/supabase/migrations/00003_pg_cron.sql +121 -0
  356. package/carbon/supabase/migrations/00004_contact_newsletter.sql +81 -0
  357. package/carbon/supabase/migrations/00005_localization_languages.sql +22 -0
  358. package/carbon/supabase/seed.sql +16 -0
  359. package/carbon/tests/_helpers/app.ts +45 -0
  360. package/carbon/tests/_helpers/env.ts +37 -0
  361. package/carbon/tests/_helpers/factories.ts +69 -0
  362. package/carbon/tests/_helpers/jwt.ts +23 -0
  363. package/carbon/tests/_helpers/setup-integration.ts +20 -0
  364. package/carbon/tests/_helpers/setup-rtl.ts +12 -0
  365. package/carbon/tests/component/ErrorBoundary.test.tsx +53 -0
  366. package/carbon/tests/component/use-auth-settings.test.tsx +119 -0
  367. package/carbon/tests/integration/server/routes/contact.test.ts +162 -0
  368. package/carbon/tests/integration/server/routes/health.test.ts +61 -0
  369. package/carbon/tests/structural/i18n-parity.test.ts +42 -0
  370. package/carbon/tests/unit/client/utils.test.ts +49 -0
  371. package/carbon/tests/unit/shared/pricing.test.ts +93 -0
  372. package/carbon/tsconfig.json +27 -0
  373. package/carbon/tsconfig.server.json +27 -0
  374. package/carbon/tsconfig.test.json +9 -0
  375. package/carbon/vite.config.ts +110 -0
  376. package/carbon/vitest.config.ts +74 -0
  377. package/carbon/volumes/db/jwt.sql +57 -0
  378. package/carbon/volumes/db/metabase-init.sh +29 -0
  379. package/carbon/volumes/db/n8n-init.sh +25 -0
  380. package/carbon/volumes/db/realtime.sql +33 -0
  381. package/carbon/volumes/db/roles.sql +93 -0
  382. package/carbon/volumes/db/set-passwords.sh +12 -0
  383. package/carbon/volumes/db/super-admin.dev.sql +113 -0
  384. package/carbon/volumes/db/super-admin.generated.sql +113 -0
  385. package/carbon/volumes/db/super-admin.sql +114 -0
  386. package/carbon/volumes/grafana/dashboards/logs.json +179 -0
  387. package/carbon/volumes/grafana/dashboards/overview.json +523 -0
  388. package/carbon/volumes/grafana/dashboards/postgresql.json +337 -0
  389. package/carbon/volumes/grafana/dashboards.dev/logs.json +179 -0
  390. package/carbon/volumes/grafana/dashboards.dev/overview.json +156 -0
  391. package/carbon/volumes/grafana/dashboards.dev/postgresql.json +337 -0
  392. package/carbon/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +16 -0
  393. package/carbon/volumes/grafana/provisioning/dashboards/dashboards.yml +16 -0
  394. package/carbon/volumes/grafana/provisioning/datasources/datasources.yml +43 -0
  395. package/carbon/volumes/kong/docker-entrypoint.sh +9 -0
  396. package/carbon/volumes/kong/kong.yml +208 -0
  397. package/carbon/volumes/loki/loki-config.yml +58 -0
  398. package/carbon/volumes/n8n/hooks.js +131 -0
  399. package/carbon/volumes/n8n/scripts/setup.sh +66 -0
  400. package/carbon/volumes/prometheus/prometheus.yml +43 -0
  401. package/carbon/volumes/promtail/promtail-config.yml +64 -0
  402. package/carbon/volumes/traefik/middlewares.dev.yml +83 -0
  403. package/carbon/volumes/traefik/middlewares.yml +95 -0
  404. package/carbon/volumes/traefik/vite-dev.yml +20 -0
  405. package/package.json +95 -0
  406. package/src/access.js +354 -0
  407. package/src/activate.js +187 -0
  408. package/src/add.js +718 -0
  409. package/src/backup.js +786 -0
  410. package/src/cli.js +350 -0
  411. package/src/configure.js +967 -0
  412. package/src/console.js +155 -0
  413. package/src/create.js +1499 -0
  414. package/src/deploy.js +311 -0
  415. package/src/destroy.js +2033 -0
  416. package/src/diagnose.js +735 -0
  417. package/src/down.js +80 -0
  418. package/src/failover.js +1032 -0
  419. package/src/lib/backup-s3.js +179 -0
  420. package/src/lib/build.js +33 -0
  421. package/src/lib/checksum.js +28 -0
  422. package/src/lib/ci-setup.js +666 -0
  423. package/src/lib/cli/help.js +129 -0
  424. package/src/lib/cli/parse-flags.js +160 -0
  425. package/src/lib/cli/select-action.js +65 -0
  426. package/src/lib/cli/select-environment.js +108 -0
  427. package/src/lib/cli/tty-guard.js +75 -0
  428. package/src/lib/cloudflare.js +447 -0
  429. package/src/lib/colors.js +52 -0
  430. package/src/lib/command.js +361 -0
  431. package/src/lib/config.js +359 -0
  432. package/src/lib/cost.js +103 -0
  433. package/src/lib/deploy/bundle.js +231 -0
  434. package/src/lib/deploy/compose/acme-verify.js +121 -0
  435. package/src/lib/deploy/compose/build-args.js +78 -0
  436. package/src/lib/deploy/compose/ha.js +1874 -0
  437. package/src/lib/deploy/compose/index.js +1294 -0
  438. package/src/lib/deploy/compose/reconcile.js +74 -0
  439. package/src/lib/deploy/github.js +382 -0
  440. package/src/lib/deploy/image.js +191 -0
  441. package/src/lib/deploy/index.js +119 -0
  442. package/src/lib/deploy/k8s/LICENSE +99 -0
  443. package/src/lib/deploy/k8s/gitops-deploy.js +298 -0
  444. package/src/lib/deploy/k8s/ha/index.js +1000 -0
  445. package/src/lib/deploy/k8s/index.js +62 -0
  446. package/src/lib/deploy/k8s/k3s.js +2515 -0
  447. package/src/lib/deploy/orchestrator.js +1401 -0
  448. package/src/lib/deploy/prompts.js +545 -0
  449. package/src/lib/deploy/remote-build.js +130 -0
  450. package/src/lib/deploy/state.js +120 -0
  451. package/src/lib/deploy/utils.js +328 -0
  452. package/src/lib/deploy-logger.js +93 -0
  453. package/src/lib/dns-propagation.js +48 -0
  454. package/src/lib/environment.js +58 -0
  455. package/src/lib/fetch-retry.js +103 -0
  456. package/src/lib/github-environments.js +335 -0
  457. package/src/lib/hetzner-dns.js +377 -0
  458. package/src/lib/hetzner-guided-setup.js +275 -0
  459. package/src/lib/host-keys.js +37 -0
  460. package/src/lib/iac/cloud-init.js +52 -0
  461. package/src/lib/iac/index.js +325 -0
  462. package/src/lib/iac/programs/hetzner-compose.js +130 -0
  463. package/src/lib/iac/programs/hetzner-k8s.js +320 -0
  464. package/src/lib/kubectl.js +81 -0
  465. package/src/lib/licensing/index.js +259 -0
  466. package/src/lib/licensing/tiers.js +181 -0
  467. package/src/lib/licensing/validator.js +171 -0
  468. package/src/lib/merge-package-json.js +90 -0
  469. package/src/lib/operator-ip.js +381 -0
  470. package/src/lib/package-manager.js +111 -0
  471. package/src/lib/perf.js +71 -0
  472. package/src/lib/project-guard.js +39 -0
  473. package/src/lib/project.js +334 -0
  474. package/src/lib/providers/base.js +276 -0
  475. package/src/lib/providers/hetzner-s3.js +656 -0
  476. package/src/lib/providers/hetzner.js +755 -0
  477. package/src/lib/providers/index.js +164 -0
  478. package/src/lib/s3.js +510 -0
  479. package/src/lib/secret-scan.js +583 -0
  480. package/src/lib/secrets.js +63 -0
  481. package/src/lib/server-types.js +195 -0
  482. package/src/lib/shell.js +91 -0
  483. package/src/lib/ssh.js +241 -0
  484. package/src/lib/tracker.js +242 -0
  485. package/src/lib/upgrade-policy.js +170 -0
  486. package/src/lib/validators.js +105 -0
  487. package/src/lib/version.js +5 -0
  488. package/src/remove.js +292 -0
  489. package/src/reset.js +97 -0
  490. package/src/restore.js +871 -0
  491. package/src/scale.js +1734 -0
  492. package/src/shell.js +222 -0
  493. package/src/status.js +981 -0
  494. package/src/up.js +264 -0
  495. package/src/upgrade.js +721 -0
@@ -0,0 +1,87 @@
1
+ import type { Plan, PlanId } from '@shared/pricing';
2
+ import { plans } from '@shared/pricing';
3
+ import { useQuery } from '@tanstack/react-query';
4
+ import { supabase } from '@/lib/supabase';
5
+
6
+ interface Subscription {
7
+ id: string;
8
+ status: string;
9
+ priceId: string;
10
+ planId: PlanId;
11
+ currentPeriodEnd: string;
12
+ cancelAtPeriodEnd: boolean;
13
+ product: {
14
+ id: string;
15
+ name: string;
16
+ description?: string;
17
+ };
18
+ price: {
19
+ id: string;
20
+ unitAmount: number;
21
+ currency: string;
22
+ interval: string;
23
+ };
24
+ }
25
+
26
+ interface SubscriptionResponse {
27
+ subscription: Subscription | null;
28
+ status: string;
29
+ }
30
+
31
+ async function getAuthHeaders() {
32
+ const {
33
+ data: { session },
34
+ } = await supabase.auth.getSession();
35
+ return {
36
+ 'Content-Type': 'application/json',
37
+ Authorization: session ? `Bearer ${session.access_token}` : '',
38
+ };
39
+ }
40
+
41
+ async function fetchSubscription(
42
+ type: 'user' | 'organization',
43
+ organizationId?: string
44
+ ): Promise<SubscriptionResponse> {
45
+ const headers = await getAuthHeaders();
46
+ const params = new URLSearchParams({ type });
47
+ if (organizationId) params.set('organizationId', organizationId);
48
+
49
+ const response = await fetch(`/api/v1/billing/subscription?${params}`, { headers });
50
+ if (!response.ok) throw new Error('Failed to fetch subscription');
51
+ return response.json();
52
+ }
53
+
54
+ export const subscriptionQueryKey = (type: 'user' | 'organization', organizationId?: string) =>
55
+ ['subscription', type, organizationId ?? 'self'] as const;
56
+
57
+ export function useSubscription(options?: {
58
+ type?: 'user' | 'organization';
59
+ organizationId?: string;
60
+ enabled?: boolean;
61
+ }) {
62
+ const type = options?.type ?? 'user';
63
+ const organizationId = options?.organizationId;
64
+
65
+ const query = useQuery({
66
+ queryKey: subscriptionQueryKey(type, organizationId),
67
+ queryFn: () => fetchSubscription(type, organizationId),
68
+ staleTime: 2 * 60 * 1000,
69
+ gcTime: 5 * 60 * 1000,
70
+ enabled: options?.enabled ?? true,
71
+ });
72
+
73
+ const subscription = query.data?.subscription ?? null;
74
+ const planId: PlanId = subscription?.planId ?? 'free';
75
+ const plan: Plan = plans.find((p) => p.id === planId) ?? plans[0];
76
+ const isActive = subscription?.status === 'active' || subscription?.status === 'trialing';
77
+
78
+ return {
79
+ ...query,
80
+ subscription,
81
+ planId,
82
+ plan,
83
+ isActive,
84
+ isTrialing: subscription?.status === 'trialing',
85
+ isCanceling: subscription?.cancelAtPeriodEnd ?? false,
86
+ };
87
+ }
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+
5
+ const MOBILE_BREAKPOINT = 768;
6
+
7
+ export function useIsMobile() {
8
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
9
+
10
+ React.useEffect(() => {
11
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
12
+ const onChange = () => {
13
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
14
+ };
15
+ mql.addEventListener('change', onChange);
16
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
17
+ return () => mql.removeEventListener('change', onChange);
18
+ }, []);
19
+
20
+ return !!isMobile;
21
+ }
@@ -0,0 +1,91 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ interface MousePosition {
4
+ x: number;
5
+ y: number;
6
+ clientX: number;
7
+ clientY: number;
8
+ }
9
+
10
+ /**
11
+ * Global mouse position tracking with RAF throttling for performance
12
+ */
13
+ export function useMousePosition(): MousePosition {
14
+ const [position, setPosition] = useState<MousePosition>({
15
+ x: 0,
16
+ y: 0,
17
+ clientX: 0,
18
+ clientY: 0,
19
+ });
20
+ const rafRef = useRef<number | null>(null);
21
+
22
+ useEffect(() => {
23
+ const handleMouseMove = (e: MouseEvent) => {
24
+ if (rafRef.current) {
25
+ cancelAnimationFrame(rafRef.current);
26
+ }
27
+ rafRef.current = requestAnimationFrame(() => {
28
+ setPosition({
29
+ x: e.pageX,
30
+ y: e.pageY,
31
+ clientX: e.clientX,
32
+ clientY: e.clientY,
33
+ });
34
+ });
35
+ };
36
+
37
+ window.addEventListener('mousemove', handleMouseMove, { passive: true });
38
+ return () => {
39
+ window.removeEventListener('mousemove', handleMouseMove);
40
+ if (rafRef.current) {
41
+ cancelAnimationFrame(rafRef.current);
42
+ }
43
+ };
44
+ }, []);
45
+
46
+ return position;
47
+ }
48
+
49
+ interface ProximityResult {
50
+ distance: number;
51
+ angle: number;
52
+ relativeX: number;
53
+ relativeY: number;
54
+ }
55
+
56
+ /**
57
+ * Calculate proximity of mouse to an element
58
+ * Useful for proximity glow and magnetic effects
59
+ */
60
+ export function useElementProximity(
61
+ elementRef: React.RefObject<HTMLElement | null>
62
+ ): ProximityResult {
63
+ const mousePosition = useMousePosition();
64
+ const [proximity, setProximity] = useState<ProximityResult>({
65
+ distance: Number.POSITIVE_INFINITY,
66
+ angle: 0,
67
+ relativeX: 50,
68
+ relativeY: 50,
69
+ });
70
+
71
+ useEffect(() => {
72
+ if (!elementRef.current) return;
73
+
74
+ const rect = elementRef.current.getBoundingClientRect();
75
+ const centerX = rect.left + rect.width / 2;
76
+ const centerY = rect.top + rect.height / 2;
77
+
78
+ const dx = mousePosition.clientX - centerX;
79
+ const dy = mousePosition.clientY - centerY;
80
+ const distance = Math.sqrt(dx * dx + dy * dy);
81
+ const angle = Math.atan2(dy, dx);
82
+
83
+ // Relative position as percentage (0-100)
84
+ const relativeX = ((mousePosition.clientX - rect.left) / rect.width) * 100;
85
+ const relativeY = ((mousePosition.clientY - rect.top) / rect.height) * 100;
86
+
87
+ setProximity({ distance, angle, relativeX, relativeY });
88
+ }, [mousePosition, elementRef]);
89
+
90
+ return proximity;
91
+ }
@@ -0,0 +1,124 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import { createContext, type ReactNode, useCallback, useContext, useState } from 'react';
3
+ import { useAuth } from '@/components/auth/AuthProvider';
4
+ import { getAuthHeaders } from '@/lib/api';
5
+
6
+ export type NotificationType = 'info' | 'warning' | 'error' | 'success';
7
+ export type NotificationVisibility = 'all' | 'authenticated' | 'public';
8
+
9
+ export type Notification = {
10
+ id: string;
11
+ title: string;
12
+ message?: string;
13
+ type: NotificationType;
14
+ visibility?: NotificationVisibility;
15
+ dismissible: boolean;
16
+ action?: {
17
+ label: string;
18
+ url: string;
19
+ };
20
+ createdAt: string;
21
+ };
22
+
23
+ type NotificationContextValue = {
24
+ notifications: Notification[];
25
+ hasNotifications: boolean;
26
+ isLoading: boolean;
27
+ isDrawerOpen: boolean;
28
+ openDrawer: () => void;
29
+ closeDrawer: () => void;
30
+ toggleDrawer: () => void;
31
+ dismissNotification: (id: string) => Promise<void>;
32
+ refetch: () => void;
33
+ };
34
+
35
+ const NotificationContext = createContext<NotificationContextValue | null>(null);
36
+
37
+ async function fetchNotifications(authenticated: boolean): Promise<Notification[]> {
38
+ const headers: HeadersInit = authenticated ? await getAuthHeaders() : {};
39
+ const response = await fetch('/api/v1/notifications', { headers });
40
+
41
+ if (!response.ok) {
42
+ if (response.status === 401) {
43
+ return [];
44
+ }
45
+ throw new Error('Failed to fetch notifications');
46
+ }
47
+
48
+ const data = await response.json();
49
+ return data.notifications;
50
+ }
51
+
52
+ async function dismissNotificationApi(notificationId: string): Promise<void> {
53
+ const headers = await getAuthHeaders();
54
+ const response = await fetch(`/api/v1/notifications/${notificationId}/dismiss`, {
55
+ method: 'POST',
56
+ headers,
57
+ });
58
+
59
+ if (!response.ok) {
60
+ const error = await response.json();
61
+ throw new Error(error.error || 'Failed to dismiss notification');
62
+ }
63
+ }
64
+
65
+ export function NotificationProvider({ children }: { children: ReactNode }) {
66
+ const { user } = useAuth();
67
+ const queryClient = useQueryClient();
68
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
69
+
70
+ const {
71
+ data: notifications = [],
72
+ isLoading,
73
+ refetch,
74
+ } = useQuery({
75
+ queryKey: ['notifications', !!user],
76
+ queryFn: () => fetchNotifications(!!user),
77
+ staleTime: Infinity, // Only fetch on page load
78
+ refetchOnWindowFocus: false,
79
+ });
80
+
81
+ const dismissMutation = useMutation({
82
+ mutationFn: dismissNotificationApi,
83
+ onSuccess: () => {
84
+ queryClient.invalidateQueries({ queryKey: ['notifications'] });
85
+ },
86
+ });
87
+
88
+ const openDrawer = useCallback(() => setIsDrawerOpen(true), []);
89
+ const closeDrawer = useCallback(() => setIsDrawerOpen(false), []);
90
+ const toggleDrawer = useCallback(() => setIsDrawerOpen((prev) => !prev), []);
91
+
92
+ const dismissNotification = useCallback(
93
+ async (id: string) => {
94
+ await dismissMutation.mutateAsync(id);
95
+ },
96
+ [dismissMutation]
97
+ );
98
+
99
+ return (
100
+ <NotificationContext.Provider
101
+ value={{
102
+ notifications,
103
+ hasNotifications: notifications.length > 0,
104
+ isLoading,
105
+ isDrawerOpen,
106
+ openDrawer,
107
+ closeDrawer,
108
+ toggleDrawer,
109
+ dismissNotification,
110
+ refetch,
111
+ }}
112
+ >
113
+ {children}
114
+ </NotificationContext.Provider>
115
+ );
116
+ }
117
+
118
+ export function useNotifications() {
119
+ const context = useContext(NotificationContext);
120
+ if (!context) {
121
+ throw new Error('useNotifications must be used within a NotificationProvider');
122
+ }
123
+ return context;
124
+ }
@@ -0,0 +1,127 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import { getAuthHeaders } from '@/lib/api';
3
+
4
+ export type MemberRole = 'OWNER' | 'ADMIN' | 'MEMBER';
5
+
6
+ export type Member = {
7
+ id: string;
8
+ role: MemberRole;
9
+ createdAt: string;
10
+ user: {
11
+ id: string;
12
+ email: string;
13
+ name?: string;
14
+ avatar?: string;
15
+ } | null;
16
+ };
17
+
18
+ async function fetchMembers(orgId: string): Promise<Member[]> {
19
+ const headers = await getAuthHeaders();
20
+ const response = await fetch(`/api/v1/organizations/${orgId}/members`, { headers });
21
+
22
+ if (!response.ok) {
23
+ const error = await response.json();
24
+ throw new Error(error.error || 'Failed to fetch members');
25
+ }
26
+
27
+ const data = await response.json();
28
+ return data.members;
29
+ }
30
+
31
+ async function addMember(orgId: string, email: string, role: 'ADMIN' | 'MEMBER'): Promise<Member> {
32
+ const headers = await getAuthHeaders();
33
+ const response = await fetch(`/api/v1/organizations/${orgId}/members`, {
34
+ method: 'POST',
35
+ headers,
36
+ body: JSON.stringify({ email, role }),
37
+ });
38
+
39
+ if (!response.ok) {
40
+ const error = await response.json();
41
+ throw new Error(error.error || 'Failed to add member');
42
+ }
43
+
44
+ const data = await response.json();
45
+ return data.member;
46
+ }
47
+
48
+ async function updateMemberRole(orgId: string, userId: string, role: MemberRole): Promise<void> {
49
+ const headers = await getAuthHeaders();
50
+ const response = await fetch(`/api/v1/organizations/${orgId}/members/${userId}`, {
51
+ method: 'PATCH',
52
+ headers,
53
+ body: JSON.stringify({ role }),
54
+ });
55
+
56
+ if (!response.ok) {
57
+ const error = await response.json();
58
+ throw new Error(error.error || 'Failed to update role');
59
+ }
60
+ }
61
+
62
+ async function removeMember(orgId: string, userId: string): Promise<void> {
63
+ const headers = await getAuthHeaders();
64
+ const response = await fetch(`/api/v1/organizations/${orgId}/members/${userId}`, {
65
+ method: 'DELETE',
66
+ headers,
67
+ });
68
+
69
+ if (!response.ok) {
70
+ const error = await response.json();
71
+ throw new Error(error.error || 'Failed to remove member');
72
+ }
73
+ }
74
+
75
+ export function useOrganizationMembers(orgId: string | undefined) {
76
+ const queryClient = useQueryClient();
77
+ const queryKey = ['organization-members', orgId];
78
+
79
+ const membersQuery = useQuery({
80
+ queryKey,
81
+ queryFn: () => {
82
+ if (!orgId) throw new Error('Organization ID is required');
83
+ return fetchMembers(orgId);
84
+ },
85
+ enabled: !!orgId,
86
+ });
87
+
88
+ const addMemberMutation = useMutation({
89
+ mutationFn: ({ email, role }: { email: string; role: 'ADMIN' | 'MEMBER' }) => {
90
+ if (!orgId) throw new Error('Organization ID is required');
91
+ return addMember(orgId, email, role);
92
+ },
93
+ onSuccess: () => {
94
+ queryClient.invalidateQueries({ queryKey });
95
+ },
96
+ });
97
+
98
+ const updateRoleMutation = useMutation({
99
+ mutationFn: ({ userId, role }: { userId: string; role: MemberRole }) => {
100
+ if (!orgId) throw new Error('Organization ID is required');
101
+ return updateMemberRole(orgId, userId, role);
102
+ },
103
+ onSuccess: () => {
104
+ queryClient.invalidateQueries({ queryKey });
105
+ },
106
+ });
107
+
108
+ const removeMemberMutation = useMutation({
109
+ mutationFn: (userId: string) => {
110
+ if (!orgId) throw new Error('Organization ID is required');
111
+ return removeMember(orgId, userId);
112
+ },
113
+ onSuccess: () => {
114
+ queryClient.invalidateQueries({ queryKey });
115
+ },
116
+ });
117
+
118
+ return {
119
+ members: membersQuery.data ?? [],
120
+ isLoading: membersQuery.isLoading,
121
+ error: membersQuery.error,
122
+ refetch: membersQuery.refetch,
123
+ addMember: addMemberMutation,
124
+ updateRole: updateRoleMutation,
125
+ removeMember: removeMemberMutation,
126
+ };
127
+ }
@@ -0,0 +1,230 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import {
3
+ createContext,
4
+ type ReactNode,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useState,
10
+ } from 'react';
11
+ import { useAuth } from '@/components/auth/AuthProvider';
12
+
13
+ export type OrganizationRole = 'OWNER' | 'ADMIN' | 'MEMBER';
14
+
15
+ export type Organization = {
16
+ id: string;
17
+ name: string;
18
+ slug: string;
19
+ plan: 'FREE' | 'STARTER' | 'PRO' | 'ENTERPRISE';
20
+ created_at: string;
21
+ updated_at: string;
22
+ };
23
+
24
+ export type Membership = {
25
+ id: string;
26
+ role: OrganizationRole;
27
+ organization: Organization;
28
+ };
29
+
30
+ type OrganizationsContextType = {
31
+ /** List of organizations the user belongs to */
32
+ organizations: Organization[];
33
+ /** List of memberships with role info */
34
+ memberships: Membership[];
35
+ /** Currently selected organization */
36
+ selectedOrganization: Organization | null;
37
+ /** User's role in the selected organization */
38
+ selectedOrganizationRole: OrganizationRole | null;
39
+ /** Whether the user is an admin (OWNER or ADMIN) of the selected organization */
40
+ isOrgAdmin: boolean;
41
+ /** Whether the user is the owner of the selected organization */
42
+ isOrgOwner: boolean;
43
+ /** Loading state */
44
+ isLoading: boolean;
45
+ /** Error state */
46
+ error: string | null;
47
+ /** Select an organization */
48
+ selectOrganization: (org: Organization) => void;
49
+ /** Create a new organization */
50
+ createOrganization: (name: string) => Promise<Organization>;
51
+ /** Refresh organizations list */
52
+ refresh: () => Promise<void>;
53
+ };
54
+
55
+ const OrganizationsContext = createContext<OrganizationsContextType | undefined>(undefined);
56
+
57
+ const SELECTED_ORG_KEY = 'selectedOrganizationId';
58
+
59
+ function generateSlug(name: string): string {
60
+ return name
61
+ .toLowerCase()
62
+ .trim()
63
+ .replace(/[^a-z0-9\s-]/g, '')
64
+ .replace(/\s+/g, '-')
65
+ .replace(/-+/g, '-')
66
+ .substring(0, 50);
67
+ }
68
+
69
+ function transformMemberships(data: Record<string, unknown>[]): Membership[] {
70
+ return (data || [])
71
+ .filter((m) => m.organization !== null)
72
+ .map((m) => ({
73
+ id: m.id as string,
74
+ role: m.role as OrganizationRole,
75
+ organization: m.organization as unknown as Organization,
76
+ }));
77
+ }
78
+
79
+ export function OrganizationsProvider({ children }: { children: ReactNode }) {
80
+ const { user, session } = useAuth();
81
+ const queryClient = useQueryClient();
82
+ const [selectedOrganization, setSelectedOrganization] = useState<Organization | null>(null);
83
+
84
+ const {
85
+ data: memberships = [],
86
+ isLoading,
87
+ error: queryError,
88
+ refetch,
89
+ } = useQuery({
90
+ queryKey: ['organizations', user?.id],
91
+ queryFn: async () => {
92
+ const response = await fetch('/api/v1/me', {
93
+ headers: { Authorization: `Bearer ${session?.access_token}` },
94
+ });
95
+
96
+ if (!response.ok) {
97
+ throw new Error('Failed to fetch organizations');
98
+ }
99
+
100
+ const { memberships: data } = await response.json();
101
+ return transformMemberships(data);
102
+ },
103
+ enabled: !!user && !!session,
104
+ staleTime: 1000 * 60 * 5, // 5 minutes
105
+ });
106
+
107
+ const error = queryError
108
+ ? queryError instanceof Error
109
+ ? queryError.message
110
+ : 'Failed to fetch organizations'
111
+ : null;
112
+
113
+ // Restore selected organization from localStorage when memberships change
114
+ useEffect(() => {
115
+ if (memberships.length === 0) {
116
+ setSelectedOrganization(null);
117
+ return;
118
+ }
119
+
120
+ const savedOrgId = localStorage.getItem(SELECTED_ORG_KEY);
121
+ const savedOrg = memberships.find((m) => m.organization.id === savedOrgId);
122
+
123
+ if (savedOrg) {
124
+ setSelectedOrganization(savedOrg.organization);
125
+ } else {
126
+ setSelectedOrganization(memberships[0].organization);
127
+ }
128
+ }, [memberships]);
129
+
130
+ // Select organization handler
131
+ const selectOrganization = useCallback((org: Organization) => {
132
+ setSelectedOrganization(org);
133
+ localStorage.setItem(SELECTED_ORG_KEY, org.id);
134
+ }, []);
135
+
136
+ // Create organization mutation
137
+ const createMutation = useMutation({
138
+ mutationFn: async (name: string) => {
139
+ if (!session) {
140
+ throw new Error('Not authenticated');
141
+ }
142
+
143
+ const slug = generateSlug(name);
144
+
145
+ const response = await fetch('/api/v1/organizations', {
146
+ method: 'POST',
147
+ headers: {
148
+ 'Content-Type': 'application/json',
149
+ Authorization: `Bearer ${session.access_token}`,
150
+ },
151
+ body: JSON.stringify({ name, slug }),
152
+ });
153
+
154
+ const data = await response.json();
155
+
156
+ if (!response.ok) {
157
+ throw new Error(data.error || 'Failed to create organization');
158
+ }
159
+
160
+ return data.organization as Organization;
161
+ },
162
+ onSuccess: (org) => {
163
+ queryClient.invalidateQueries({ queryKey: ['organizations'] });
164
+ selectOrganization(org);
165
+ },
166
+ });
167
+
168
+ const createOrganization = useCallback(
169
+ async (name: string): Promise<Organization> => {
170
+ return createMutation.mutateAsync(name);
171
+ },
172
+ [createMutation]
173
+ );
174
+
175
+ const refresh = useCallback(async () => {
176
+ await refetch();
177
+ }, [refetch]);
178
+
179
+ // Derived values
180
+ const organizations = useMemo(() => memberships.map((m) => m.organization), [memberships]);
181
+
182
+ const selectedMembership = useMemo(
183
+ () => memberships.find((m) => m.organization.id === selectedOrganization?.id),
184
+ [memberships, selectedOrganization]
185
+ );
186
+
187
+ const selectedOrganizationRole = selectedMembership?.role ?? null;
188
+ const isOrgAdmin = selectedOrganizationRole === 'OWNER' || selectedOrganizationRole === 'ADMIN';
189
+ const isOrgOwner = selectedOrganizationRole === 'OWNER';
190
+
191
+ const value = useMemo(
192
+ () => ({
193
+ organizations,
194
+ memberships,
195
+ selectedOrganization,
196
+ selectedOrganizationRole,
197
+ isOrgAdmin,
198
+ isOrgOwner,
199
+ isLoading: !!user && isLoading,
200
+ error,
201
+ selectOrganization,
202
+ createOrganization,
203
+ refresh,
204
+ }),
205
+ [
206
+ organizations,
207
+ memberships,
208
+ selectedOrganization,
209
+ selectedOrganizationRole,
210
+ isOrgAdmin,
211
+ isOrgOwner,
212
+ user,
213
+ isLoading,
214
+ error,
215
+ selectOrganization,
216
+ createOrganization,
217
+ refresh,
218
+ ]
219
+ );
220
+
221
+ return <OrganizationsContext.Provider value={value}>{children}</OrganizationsContext.Provider>;
222
+ }
223
+
224
+ export function useOrganizations() {
225
+ const context = useContext(OrganizationsContext);
226
+ if (context === undefined) {
227
+ throw new Error('useOrganizations must be used within an OrganizationsProvider');
228
+ }
229
+ return context;
230
+ }