@primstack/cli 0.0.1

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 (210) hide show
  1. package/dist/generators/crud/templates/drizzle-table.ts.template +12 -0
  2. package/dist/generators/crud/templates/handlers.ts.template +136 -0
  3. package/dist/generators/crud/templates/routes.ts.template +21 -0
  4. package/dist/generators/crud/templates/schema.ts.template +20 -0
  5. package/dist/index.js +9618 -0
  6. package/dist/integrations/analytics/providers/amplitude/templates/src/analytics/index.ts.template +79 -0
  7. package/dist/integrations/analytics/providers/amplitude/templates/src/analytics/types.ts.template +12 -0
  8. package/dist/integrations/analytics/providers/mixpanel/templates/src/analytics/index.ts.template +62 -0
  9. package/dist/integrations/analytics/providers/mixpanel/templates/src/analytics/types.ts.template +12 -0
  10. package/dist/integrations/analytics/providers/posthog/templates/src/analytics/index.ts.template +67 -0
  11. package/dist/integrations/analytics/providers/posthog/templates/src/analytics/types.ts.template +12 -0
  12. package/dist/integrations/auth/providers/authjoy/templates/api/src/middleware/auth.ts.template +89 -0
  13. package/dist/integrations/auth/providers/authjoy/templates/api/src/routes/auth.ts.template +27 -0
  14. package/dist/integrations/auth/providers/authjoy/templates/web/src/components/AuthProvider.tsx.template +40 -0
  15. package/dist/integrations/auth/providers/authjoy/templates/web/src/hooks/use-auth.ts.template +71 -0
  16. package/dist/integrations/auth/providers/authjoy/templates/web/src/lib/auth.ts.template +59 -0
  17. package/dist/integrations/auth/providers/authjoy/templates/web/src/pages/account.tsx.template +84 -0
  18. package/dist/integrations/auth/providers/authjoy/templates/web/src/pages/login.tsx.template +73 -0
  19. package/dist/integrations/cache/providers/memory/templates/src/cache/index.ts.template +43 -0
  20. package/dist/integrations/cache/providers/memory/templates/src/cache/types.ts.template +3 -0
  21. package/dist/integrations/cache/providers/redis/templates/src/cache/index.ts.template +37 -0
  22. package/dist/integrations/cache/providers/redis/templates/src/cache/types.ts.template +3 -0
  23. package/dist/integrations/cache/providers/valkey/templates/src/cache/index.ts.template +38 -0
  24. package/dist/integrations/cache/providers/valkey/templates/src/cache/types.ts.template +3 -0
  25. package/dist/integrations/db/providers/postgres/templates/drizzle.config.ts.template +10 -0
  26. package/dist/integrations/db/providers/postgres/templates/src/db/index.ts.template +13 -0
  27. package/dist/integrations/db/providers/postgres/templates/src/db/migrate.ts.template +19 -0
  28. package/dist/integrations/db/providers/postgres/templates/src/db/schema/index.ts.template +1 -0
  29. package/dist/integrations/db/providers/postgres/templates/src/db/schema/users.ts.template +12 -0
  30. package/dist/integrations/db/providers/postgres/templates/src/db/seed.ts.template +28 -0
  31. package/dist/integrations/db/providers/sqlite/templates/drizzle.config.ts.template +10 -0
  32. package/dist/integrations/db/providers/sqlite/templates/src/db/index.ts.template +10 -0
  33. package/dist/integrations/db/providers/sqlite/templates/src/db/schema/index.ts.template +1 -0
  34. package/dist/integrations/db/providers/sqlite/templates/src/db/schema/users.ts.template +12 -0
  35. package/dist/integrations/db/providers/sqlite/templates/src/db/seed.ts.template +28 -0
  36. package/dist/integrations/db/providers/supabase/templates/drizzle.config.ts.template +10 -0
  37. package/dist/integrations/db/providers/supabase/templates/src/db/index.ts.template +13 -0
  38. package/dist/integrations/db/providers/supabase/templates/src/db/migrate.ts.template +19 -0
  39. package/dist/integrations/db/providers/supabase/templates/src/db/schema/index.ts.template +1 -0
  40. package/dist/integrations/db/providers/supabase/templates/src/db/schema/users.ts.template +12 -0
  41. package/dist/integrations/db/providers/supabase/templates/src/db/seed.ts.template +28 -0
  42. package/dist/integrations/db/providers/turso/templates/drizzle.config.ts.template +11 -0
  43. package/dist/integrations/db/providers/turso/templates/src/db/index.ts.template +14 -0
  44. package/dist/integrations/db/providers/turso/templates/src/db/schema/index.ts.template +1 -0
  45. package/dist/integrations/db/providers/turso/templates/src/db/schema/users.ts.template +12 -0
  46. package/dist/integrations/db/providers/turso/templates/src/db/seed.ts.template +28 -0
  47. package/dist/integrations/email/providers/nodemailer/templates/src/email/index.ts.template +24 -0
  48. package/dist/integrations/email/providers/nodemailer/templates/src/email/templates/index.ts.template +1 -0
  49. package/dist/integrations/email/providers/nodemailer/templates/src/email/templates/welcome.ts.template +7 -0
  50. package/dist/integrations/email/providers/nodemailer/templates/src/email/types.ts.template +7 -0
  51. package/dist/integrations/email/providers/resend/templates/src/email/index.ts.template +18 -0
  52. package/dist/integrations/email/providers/resend/templates/src/email/templates/index.ts.template +1 -0
  53. package/dist/integrations/email/providers/resend/templates/src/email/templates/welcome.ts.template +7 -0
  54. package/dist/integrations/email/providers/resend/templates/src/email/types.ts.template +7 -0
  55. package/dist/integrations/email/providers/sendgrid/templates/src/email/index.ts.template +16 -0
  56. package/dist/integrations/email/providers/sendgrid/templates/src/email/templates/index.ts.template +1 -0
  57. package/dist/integrations/email/providers/sendgrid/templates/src/email/templates/welcome.ts.template +7 -0
  58. package/dist/integrations/email/providers/sendgrid/templates/src/email/types.ts.template +7 -0
  59. package/dist/integrations/flags/providers/local/templates/api/src/lib/flags.ts.template +97 -0
  60. package/dist/integrations/flags/providers/local/templates/api/src/routes/flags.ts.template +36 -0
  61. package/dist/integrations/flags/providers/local/templates/flags.json.template +8 -0
  62. package/dist/integrations/flags/providers/local/templates/web/src/hooks/use-flag.ts.template +60 -0
  63. package/dist/integrations/logging/providers/axiom/templates/src/logging/index.ts.template +56 -0
  64. package/dist/integrations/logging/providers/axiom/templates/src/logging/types.ts.template +5 -0
  65. package/dist/integrations/logging/providers/pino/templates/src/logging/index.ts.template +21 -0
  66. package/dist/integrations/logging/providers/pino/templates/src/logging/types.ts.template +5 -0
  67. package/dist/integrations/logging/providers/winston/templates/src/logging/index.ts.template +30 -0
  68. package/dist/integrations/logging/providers/winston/templates/src/logging/types.ts.template +5 -0
  69. package/dist/integrations/monitor/providers/datadog/templates/src/monitor/index.ts.template +78 -0
  70. package/dist/integrations/monitor/providers/datadog/templates/src/monitor/types.ts.template +12 -0
  71. package/dist/integrations/monitor/providers/newrelic/templates/src/monitor/index.ts.template +60 -0
  72. package/dist/integrations/monitor/providers/newrelic/templates/src/monitor/types.ts.template +12 -0
  73. package/dist/integrations/monitor/providers/sentry/templates/src/monitor/index.ts.template +70 -0
  74. package/dist/integrations/monitor/providers/sentry/templates/src/monitor/types.ts.template +12 -0
  75. package/dist/integrations/queue/providers/bullmq/templates/src/queue/index.ts.template +56 -0
  76. package/dist/integrations/queue/providers/bullmq/templates/src/queue/types.ts.template +10 -0
  77. package/dist/integrations/queue/providers/memory/templates/src/queue/index.ts.template +73 -0
  78. package/dist/integrations/queue/providers/memory/templates/src/queue/types.ts.template +10 -0
  79. package/dist/integrations/queue/providers/pgboss/templates/src/queue/index.ts.template +34 -0
  80. package/dist/integrations/queue/providers/pgboss/templates/src/queue/types.ts.template +10 -0
  81. package/dist/integrations/ratelimit/providers/memory/templates/src/ratelimit/index.ts.template +95 -0
  82. package/dist/integrations/ratelimit/providers/memory/templates/src/ratelimit/types.ts.template +12 -0
  83. package/dist/integrations/ratelimit/providers/rate-limiter-flexible/templates/src/ratelimit/index.ts.template +80 -0
  84. package/dist/integrations/ratelimit/providers/rate-limiter-flexible/templates/src/ratelimit/types.ts.template +12 -0
  85. package/dist/integrations/ratelimit/providers/upstash/templates/src/ratelimit/index.ts.template +67 -0
  86. package/dist/integrations/ratelimit/providers/upstash/templates/src/ratelimit/types.ts.template +12 -0
  87. package/dist/integrations/schedule/providers/bullmq/templates/src/schedule/index.ts.template +81 -0
  88. package/dist/integrations/schedule/providers/bullmq/templates/src/schedule/types.ts.template +10 -0
  89. package/dist/integrations/schedule/providers/croner/templates/src/schedule/index.ts.template +47 -0
  90. package/dist/integrations/schedule/providers/croner/templates/src/schedule/types.ts.template +10 -0
  91. package/dist/integrations/schedule/providers/node-cron/templates/src/schedule/index.ts.template +45 -0
  92. package/dist/integrations/schedule/providers/node-cron/templates/src/schedule/types.ts.template +10 -0
  93. package/dist/integrations/search/providers/algolia/templates/src/search/index.ts.template +52 -0
  94. package/dist/integrations/search/providers/algolia/templates/src/search/types.ts.template +18 -0
  95. package/dist/integrations/search/providers/meilisearch/templates/src/search/index.ts.template +49 -0
  96. package/dist/integrations/search/providers/meilisearch/templates/src/search/types.ts.template +18 -0
  97. package/dist/integrations/search/providers/typesense/templates/src/search/index.ts.template +71 -0
  98. package/dist/integrations/search/providers/typesense/templates/src/search/types.ts.template +35 -0
  99. package/dist/integrations/storage/providers/local/templates/src/storage/index.ts.template +69 -0
  100. package/dist/integrations/storage/providers/local/templates/src/storage/types.ts.template +12 -0
  101. package/dist/integrations/storage/providers/r2/templates/src/storage/index.ts.template +80 -0
  102. package/dist/integrations/storage/providers/r2/templates/src/storage/types.ts.template +12 -0
  103. package/dist/integrations/storage/providers/s3/templates/src/storage/index.ts.template +78 -0
  104. package/dist/integrations/storage/providers/s3/templates/src/storage/types.ts.template +12 -0
  105. package/dist/integrations/stripe/templates/api/src/lib/stripe.ts.template +259 -0
  106. package/dist/integrations/stripe/templates/api/src/routes/stripe-webhooks.ts.template +284 -0
  107. package/dist/integrations/stripe/templates/api/stripe.config.ts.template +178 -0
  108. package/dist/integrations/stripe/templates/shared/src/pricing.ts.template +117 -0
  109. package/dist/integrations/stripe/templates/shared/src/stripe-types.ts.template +133 -0
  110. package/dist/integrations/stripe/templates/web/src/components/billing-settings.tsx.template +123 -0
  111. package/dist/integrations/stripe/templates/web/src/components/pricing-cards.tsx.template +115 -0
  112. package/dist/integrations/stripe/templates/web/src/pages/pricing.tsx.template +95 -0
  113. package/dist/templates/api/fastify/.env.example.template +7 -0
  114. package/dist/templates/api/fastify/.gitignore.template +24 -0
  115. package/dist/templates/api/fastify/package.json.template +23 -0
  116. package/dist/templates/api/fastify/src/index.ts.template +52 -0
  117. package/dist/templates/api/fastify/src/lib/env.ts.template +20 -0
  118. package/dist/templates/api/fastify/src/routes/health.ts.template +12 -0
  119. package/dist/templates/api/fastify/tsconfig.json.template +18 -0
  120. package/dist/templates/api/fastify-postgres/.env.example.template +10 -0
  121. package/dist/templates/api/fastify-postgres/drizzle.config.ts.template +10 -0
  122. package/dist/templates/api/fastify-postgres/package.json.template +16 -0
  123. package/dist/templates/api/fastify-postgres/src/db/index.ts.template +9 -0
  124. package/dist/templates/api/fastify-postgres/src/db/schema/users.ts.template +12 -0
  125. package/dist/templates/api/fastify-sqlite/.env.example.template +10 -0
  126. package/dist/templates/api/fastify-sqlite/drizzle.config.ts.template +10 -0
  127. package/dist/templates/api/fastify-sqlite/package.json.template +16 -0
  128. package/dist/templates/api/fastify-sqlite/src/db/index.ts.template +6 -0
  129. package/dist/templates/api/fastify-sqlite/src/db/schema/users.ts.template +12 -0
  130. package/dist/templates/api/fastify-supabase/.env.example.template +10 -0
  131. package/dist/templates/api/fastify-supabase/drizzle.config.ts.template +10 -0
  132. package/dist/templates/api/fastify-supabase/package.json.template +15 -0
  133. package/dist/templates/api/fastify-supabase/src/db/index.ts.template +9 -0
  134. package/dist/templates/api/fastify-supabase/src/db/schema/users.ts.template +12 -0
  135. package/dist/templates/api/fastify-turso/.env.example.template +11 -0
  136. package/dist/templates/api/fastify-turso/drizzle.config.ts.template +11 -0
  137. package/dist/templates/api/fastify-turso/package.json.template +15 -0
  138. package/dist/templates/api/fastify-turso/src/db/index.ts.template +10 -0
  139. package/dist/templates/api/fastify-turso/src/db/schema/users.ts.template +12 -0
  140. package/dist/templates/fullstack/api/.env.example.template +10 -0
  141. package/dist/templates/fullstack/api/.gitignore.template +4 -0
  142. package/dist/templates/fullstack/api/drizzle.config.ts.template +14 -0
  143. package/dist/templates/fullstack/api/package.json.template +33 -0
  144. package/dist/templates/fullstack/api/src/db/index.ts.template +13 -0
  145. package/dist/templates/fullstack/api/src/db/schema/api-keys.ts.template +19 -0
  146. package/dist/templates/fullstack/api/src/db/schema/audit-logs.ts.template +23 -0
  147. package/dist/templates/fullstack/api/src/db/schema/index.ts.template +8 -0
  148. package/dist/templates/fullstack/api/src/db/schema/invites.ts.template +19 -0
  149. package/dist/templates/fullstack/api/src/db/schema/memberships.ts.template +16 -0
  150. package/dist/templates/fullstack/api/src/db/schema/organizations.ts.template +13 -0
  151. package/dist/templates/fullstack/api/src/db/schema/plans.ts.template +29 -0
  152. package/dist/templates/fullstack/api/src/db/schema/subscriptions.ts.template +38 -0
  153. package/dist/templates/fullstack/api/src/db/schema/users.ts.template +14 -0
  154. package/dist/templates/fullstack/api/src/index.ts.template +54 -0
  155. package/dist/templates/fullstack/api/src/lib/env.ts.template +22 -0
  156. package/dist/templates/fullstack/api/src/routes/health.ts.template +14 -0
  157. package/dist/templates/fullstack/api/tsconfig.json.template +15 -0
  158. package/dist/templates/fullstack/root/.gitignore.template +26 -0
  159. package/dist/templates/fullstack/root/package.json.template +15 -0
  160. package/dist/templates/fullstack/root/pnpm-workspace.yaml.template +3 -0
  161. package/dist/templates/fullstack/root/turbo.json.template +17 -0
  162. package/dist/templates/fullstack/shared/package.json.template +36 -0
  163. package/dist/templates/fullstack/shared/src/index.ts.template +8 -0
  164. package/dist/templates/fullstack/shared/src/schemas/api-key.ts.template +28 -0
  165. package/dist/templates/fullstack/shared/src/schemas/audit-log.ts.template +41 -0
  166. package/dist/templates/fullstack/shared/src/schemas/index.ts.template +8 -0
  167. package/dist/templates/fullstack/shared/src/schemas/invite.ts.template +25 -0
  168. package/dist/templates/fullstack/shared/src/schemas/membership.ts.template +20 -0
  169. package/dist/templates/fullstack/shared/src/schemas/organization.ts.template +18 -0
  170. package/dist/templates/fullstack/shared/src/schemas/plan.ts.template +38 -0
  171. package/dist/templates/fullstack/shared/src/schemas/subscription.ts.template +56 -0
  172. package/dist/templates/fullstack/shared/src/schemas/user.ts.template +21 -0
  173. package/dist/templates/fullstack/shared/src/types/index.ts.template +75 -0
  174. package/dist/templates/fullstack/shared/src/validators/index.ts.template +53 -0
  175. package/dist/templates/fullstack/shared/tsconfig.json.template +17 -0
  176. package/dist/templates/fullstack/web/.gitignore.template +3 -0
  177. package/dist/templates/fullstack/web/index.html.template +13 -0
  178. package/dist/templates/fullstack/web/package.json.template +23 -0
  179. package/dist/templates/fullstack/web/src/App.tsx.template +47 -0
  180. package/dist/templates/fullstack/web/src/index.css.template +54 -0
  181. package/dist/templates/fullstack/web/src/main.tsx.template +10 -0
  182. package/dist/templates/fullstack/web/src/vite-env.d.ts.template +1 -0
  183. package/dist/templates/fullstack/web/tsconfig.json.template +21 -0
  184. package/dist/templates/fullstack/web/tsconfig.node.json.template +11 -0
  185. package/dist/templates/fullstack/web/vite.config.ts.template +15 -0
  186. package/dist/templates/hosted/root/.env.local.template +13 -0
  187. package/dist/templates/hosted/root/.gitignore.template +32 -0
  188. package/dist/templates/hosted/root/CLAUDE.md.template +139 -0
  189. package/dist/templates/hosted/root/drizzle.config.ts.template +10 -0
  190. package/dist/templates/hosted/root/next.config.ts.template +15 -0
  191. package/dist/templates/hosted/root/package.json.template +40 -0
  192. package/dist/templates/hosted/root/postcss.config.mjs.template +9 -0
  193. package/dist/templates/hosted/root/primstack.config.json.template +5 -0
  194. package/dist/templates/hosted/root/tailwind.config.ts.template +14 -0
  195. package/dist/templates/hosted/root/tsconfig.json.template +25 -0
  196. package/dist/templates/hosted/root/wrangler.toml.template +9 -0
  197. package/dist/templates/hosted/src/app/actions/example.ts.template +50 -0
  198. package/dist/templates/hosted/src/app/api/health/route.ts.template +5 -0
  199. package/dist/templates/hosted/src/app/auth/login/page.tsx.template +32 -0
  200. package/dist/templates/hosted/src/app/globals.css.template +59 -0
  201. package/dist/templates/hosted/src/app/layout.tsx.template +24 -0
  202. package/dist/templates/hosted/src/app/page.tsx.template +34 -0
  203. package/dist/templates/hosted/src/db/migrations/0000_initial.sql.template +43 -0
  204. package/dist/templates/hosted/src/db/schema.ts.template +52 -0
  205. package/dist/templates/hosted/src/env.d.ts.template +10 -0
  206. package/dist/templates/hosted/src/instrumentation.ts.template +6 -0
  207. package/dist/templates/hosted/src/lib/auth.ts.template +35 -0
  208. package/dist/templates/hosted/src/lib/db.ts.template +17 -0
  209. package/dist/templates/hosted/src/middleware.ts.template +6 -0
  210. package/package.json +46 -0
@@ -0,0 +1,97 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ interface FlagDefinition {
5
+ enabled: boolean;
6
+ description?: string;
7
+ variants?: Record<string, number>;
8
+ }
9
+
10
+ interface FlagsConfig {
11
+ flags: Record<string, FlagDefinition>;
12
+ }
13
+
14
+ // Load flags from JSON file
15
+ function loadFlags(): FlagsConfig {
16
+ // Try project root first (for monorepo), then cwd
17
+ const possiblePaths = [
18
+ path.join(process.cwd(), '..', '..', 'flags.json'), // apps/api -> root
19
+ path.join(process.cwd(), 'flags.json'),
20
+ ];
21
+
22
+ const flagsPath = possiblePaths.find(p => fs.existsSync(p));
23
+
24
+ if (!flagsPath) {
25
+ return { flags: {} };
26
+ }
27
+
28
+ const content = fs.readFileSync(flagsPath, 'utf-8');
29
+ return JSON.parse(content) as FlagsConfig;
30
+ }
31
+
32
+ // Cache flags in memory (reload on each request in dev, cache in prod)
33
+ let cachedFlags: FlagsConfig | null = null;
34
+
35
+ function getFlags(): FlagsConfig {
36
+ if (process.env.NODE_ENV === 'production' && cachedFlags) {
37
+ return cachedFlags;
38
+ }
39
+ cachedFlags = loadFlags();
40
+ return cachedFlags;
41
+ }
42
+
43
+ /**
44
+ * Check if a feature flag is enabled
45
+ */
46
+ export function getFlag(key: string, defaultValue = false): boolean {
47
+ const flags = getFlags();
48
+ return flags.flags[key]?.enabled ?? defaultValue;
49
+ }
50
+
51
+ /**
52
+ * Get a deterministic variant for a user based on their ID
53
+ * Uses a simple hash to ensure the same user always gets the same variant
54
+ */
55
+ export function getFlagVariant(key: string, userId: string): string | null {
56
+ const flags = getFlags();
57
+ const flag = flags.flags[key];
58
+
59
+ if (!flag?.enabled || !flag.variants) {
60
+ return null;
61
+ }
62
+
63
+ // Simple deterministic hash based on userId and flag key
64
+ const hash = simpleHash(userId + key);
65
+ const variants = Object.entries(flag.variants);
66
+ let cumulative = 0;
67
+
68
+ for (const [variant, weight] of variants) {
69
+ cumulative += weight;
70
+ if (hash % 100 < cumulative) {
71
+ return variant;
72
+ }
73
+ }
74
+
75
+ // Fallback to first variant
76
+ return variants[0]?.[0] ?? null;
77
+ }
78
+
79
+ /**
80
+ * Get all flags (for debugging/admin)
81
+ */
82
+ export function getAllFlags(): FlagsConfig {
83
+ return getFlags();
84
+ }
85
+
86
+ /**
87
+ * Simple hash function for deterministic variant assignment
88
+ */
89
+ function simpleHash(str: string): number {
90
+ let hash = 0;
91
+ for (let i = 0; i < str.length; i++) {
92
+ const char = str.charCodeAt(i);
93
+ hash = ((hash << 5) - hash) + char;
94
+ hash = hash & hash; // Convert to 32-bit integer
95
+ }
96
+ return Math.abs(hash);
97
+ }
@@ -0,0 +1,36 @@
1
+ import { FastifyPluginAsync } from 'fastify';
2
+ import { getFlag, getFlagVariant, getAllFlags } from '../lib/flags.js';
3
+
4
+ export const flagsRoutes: FastifyPluginAsync = async (fastify) => {
5
+ // Get all flags (admin/debug)
6
+ fastify.get('/', async (request, reply) => {
7
+ // In production, you might want to protect this endpoint
8
+ const flags = getAllFlags();
9
+ return reply.send(flags);
10
+ });
11
+
12
+ // Get a specific flag
13
+ fastify.get<{ Params: { key: string } }>('/:key', async (request, reply) => {
14
+ const { key } = request.params;
15
+ const enabled = getFlag(key);
16
+ return reply.send({ key, enabled });
17
+ });
18
+
19
+ // Get variant for a flag (requires userId in query or from auth)
20
+ fastify.get<{ Params: { key: string }; Querystring: { userId?: string } }>(
21
+ '/:key/variant',
22
+ async (request, reply) => {
23
+ const { key } = request.params;
24
+ const userId = request.query.userId || (request as any).user?.id;
25
+
26
+ if (!userId) {
27
+ return reply.status(400).send({ error: 'userId is required for variant assignment' });
28
+ }
29
+
30
+ const variant = getFlagVariant(key, userId);
31
+ return reply.send({ key, variant });
32
+ }
33
+ );
34
+ };
35
+
36
+ export default flagsRoutes;
@@ -0,0 +1,8 @@
1
+ {
2
+ "flags": {
3
+ "example-feature": {
4
+ "enabled": false,
5
+ "description": "Example feature flag - delete or modify as needed"
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,60 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ /**
4
+ * Check if a feature flag is enabled
5
+ * Fetches the flag value from the API
6
+ */
7
+ export function useFlag(key: string, defaultValue = false): boolean {
8
+ const [enabled, setEnabled] = useState(defaultValue);
9
+
10
+ useEffect(() => {
11
+ fetch(`/api/flags/${key}`)
12
+ .then((res) => {
13
+ if (!res.ok) throw new Error('Failed to fetch flag');
14
+ return res.json();
15
+ })
16
+ .then((data) => setEnabled(data.enabled))
17
+ .catch(() => setEnabled(defaultValue));
18
+ }, [key, defaultValue]);
19
+
20
+ return enabled;
21
+ }
22
+
23
+ /**
24
+ * Get the variant assigned to the current user for a feature flag
25
+ * Requires userId to be passed or available from auth context
26
+ */
27
+ export function useFlagVariant(key: string, userId?: string): string | null {
28
+ const [variant, setVariant] = useState<string | null>(null);
29
+
30
+ useEffect(() => {
31
+ if (!userId) return;
32
+
33
+ fetch(`/api/flags/${key}/variant?userId=${encodeURIComponent(userId)}`)
34
+ .then((res) => {
35
+ if (!res.ok) throw new Error('Failed to fetch variant');
36
+ return res.json();
37
+ })
38
+ .then((data) => setVariant(data.variant))
39
+ .catch(() => setVariant(null));
40
+ }, [key, userId]);
41
+
42
+ return variant;
43
+ }
44
+
45
+ /**
46
+ * Sync hook that checks flag on component mount
47
+ * Useful for server-rendered pages where you want to check flags early
48
+ */
49
+ export function useFlagSync(key: string): { enabled: boolean; loading: boolean } {
50
+ const [state, setState] = useState({ enabled: false, loading: true });
51
+
52
+ useEffect(() => {
53
+ fetch(`/api/flags/${key}`)
54
+ .then((res) => res.json())
55
+ .then((data) => setState({ enabled: data.enabled, loading: false }))
56
+ .catch(() => setState({ enabled: false, loading: false }));
57
+ }, [key]);
58
+
59
+ return state;
60
+ }
@@ -0,0 +1,56 @@
1
+ import winston from 'winston';
2
+ import { WinstonTransport as AxiomTransport } from '@axiomhq/winston';
3
+
4
+ const isProduction = process.env.NODE_ENV === 'production';
5
+
6
+ const transports: winston.transport[] = [new winston.transports.Console()];
7
+
8
+ if (isProduction && process.env.AXIOM_TOKEN) {
9
+ transports.push(
10
+ new AxiomTransport({
11
+ dataset: process.env.AXIOM_DATASET || 'logs',
12
+ token: process.env.AXIOM_TOKEN!,
13
+ }),
14
+ );
15
+ }
16
+
17
+ export const logger = winston.createLogger({
18
+ level: process.env.LOG_LEVEL || 'info',
19
+ format: isProduction
20
+ ? winston.format.json()
21
+ : winston.format.combine(
22
+ winston.format.colorize(),
23
+ winston.format.simple(),
24
+ ),
25
+ transports,
26
+ });
27
+
28
+ export function createLogger(name: string) {
29
+ return winston.createLogger({
30
+ level: process.env.LOG_LEVEL || 'info',
31
+ defaultMeta: { name },
32
+ format: isProduction
33
+ ? winston.format.json()
34
+ : winston.format.combine(
35
+ winston.format.colorize(),
36
+ winston.format.simple(),
37
+ ),
38
+ transports,
39
+ });
40
+ }
41
+
42
+ export async function flushLogs(): Promise<void> {
43
+ const promises = transports.map(
44
+ (t) =>
45
+ new Promise<void>((resolve) => {
46
+ if (typeof (t as AxiomTransport).flush === 'function') {
47
+ (t as AxiomTransport).flush!().then(() => resolve()).catch(() => resolve());
48
+ } else {
49
+ resolve();
50
+ }
51
+ }),
52
+ );
53
+ await Promise.all(promises);
54
+ }
55
+
56
+ export type { LogLevel, LogContext } from './types.js';
@@ -0,0 +1,5 @@
1
+ export type LogLevel = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly';
2
+
3
+ export interface LogContext {
4
+ [key: string]: unknown;
5
+ }
@@ -0,0 +1,21 @@
1
+ import pino from 'pino';
2
+
3
+ const isProduction = process.env.NODE_ENV === 'production';
4
+
5
+ export const logger = pino({
6
+ level: process.env.LOG_LEVEL || 'info',
7
+ ...(isProduction
8
+ ? {}
9
+ : {
10
+ transport: {
11
+ target: 'pino-pretty',
12
+ options: { colorize: true },
13
+ },
14
+ }),
15
+ });
16
+
17
+ export function createLogger(name: string) {
18
+ return logger.child({ name });
19
+ }
20
+
21
+ export type { LogLevel, LogContext } from './types.js';
@@ -0,0 +1,5 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
2
+
3
+ export interface LogContext {
4
+ [key: string]: unknown;
5
+ }
@@ -0,0 +1,30 @@
1
+ import winston from 'winston';
2
+
3
+ const isProduction = process.env.NODE_ENV === 'production';
4
+
5
+ export const logger = winston.createLogger({
6
+ level: process.env.LOG_LEVEL || 'info',
7
+ format: isProduction
8
+ ? winston.format.json()
9
+ : winston.format.combine(
10
+ winston.format.colorize(),
11
+ winston.format.simple(),
12
+ ),
13
+ transports: [new winston.transports.Console()],
14
+ });
15
+
16
+ export function createLogger(name: string) {
17
+ return winston.createLogger({
18
+ level: process.env.LOG_LEVEL || 'info',
19
+ defaultMeta: { name },
20
+ format: isProduction
21
+ ? winston.format.json()
22
+ : winston.format.combine(
23
+ winston.format.colorize(),
24
+ winston.format.simple(),
25
+ ),
26
+ transports: [new winston.transports.Console()],
27
+ });
28
+ }
29
+
30
+ export type { LogLevel, LogContext } from './types.js';
@@ -0,0 +1,5 @@
1
+ export type LogLevel = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly';
2
+
3
+ export interface LogContext {
4
+ [key: string]: unknown;
5
+ }
@@ -0,0 +1,78 @@
1
+ import tracer from 'dd-trace';
2
+ import type { MonitorConfig, ErrorContext } from './types.js';
3
+
4
+ let initialized = false;
5
+
6
+ export function initMonitoring(config?: Partial<MonitorConfig>): boolean {
7
+ if (initialized) return true;
8
+
9
+ const apiKey = process.env.DD_API_KEY;
10
+ if (!apiKey) {
11
+ console.warn('[monitor] DD_API_KEY not set — monitoring disabled');
12
+ return false;
13
+ }
14
+
15
+ tracer.init({
16
+ env: config?.environment ?? process.env.DD_ENV ?? 'development',
17
+ version: config?.release,
18
+ sampleRate: config?.sampleRate ?? 1.0,
19
+ logInjection: true,
20
+ runtimeMetrics: true,
21
+ });
22
+
23
+ initialized = true;
24
+ return true;
25
+ }
26
+
27
+ export function captureError(error: Error, context?: ErrorContext): void {
28
+ if (!initialized) return;
29
+
30
+ const span = tracer.scope().active();
31
+ if (span) {
32
+ span.setTag('error', true);
33
+ span.setTag('error.message', error.message);
34
+ span.setTag('error.stack', error.stack ?? '');
35
+ if (context?.user) {
36
+ span.setTag('usr.id', context.user.id);
37
+ if (context.user.email) span.setTag('usr.email', context.user.email);
38
+ }
39
+ if (context?.tags) {
40
+ for (const [key, value] of Object.entries(context.tags)) {
41
+ span.setTag(key, value);
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ export function captureMessage(
48
+ message: string,
49
+ level: 'info' | 'warning' | 'error' = 'info',
50
+ ): void {
51
+ if (!initialized) return;
52
+
53
+ const span = tracer.scope().active();
54
+ if (span) {
55
+ span.setTag(`message.${level}`, message);
56
+ }
57
+ }
58
+
59
+ export function setUser(user: { id: string; email?: string }): void {
60
+ if (!initialized) return;
61
+
62
+ const span = tracer.scope().active();
63
+ if (span) {
64
+ span.setTag('usr.id', user.id);
65
+ if (user.email) span.setTag('usr.email', user.email);
66
+ }
67
+ }
68
+
69
+ export async function startSpan<T>(
70
+ name: string,
71
+ op: string,
72
+ callback: () => T | Promise<T>,
73
+ ): Promise<T> {
74
+ if (!initialized) return callback();
75
+ return tracer.trace(`${op}.${name}`, callback);
76
+ }
77
+
78
+ export type { MonitorConfig, ErrorContext } from './types.js';
@@ -0,0 +1,12 @@
1
+ export interface MonitorConfig {
2
+ enabled: boolean;
3
+ environment?: string;
4
+ release?: string;
5
+ sampleRate?: number;
6
+ }
7
+
8
+ export interface ErrorContext {
9
+ user?: { id: string; email?: string };
10
+ tags?: Record<string, string>;
11
+ extra?: Record<string, unknown>;
12
+ }
@@ -0,0 +1,60 @@
1
+ import newrelic from 'newrelic';
2
+ import type { MonitorConfig, ErrorContext } from './types.js';
3
+
4
+ let initialized = false;
5
+
6
+ export function initMonitoring(_config?: Partial<MonitorConfig>): boolean {
7
+ if (initialized) return true;
8
+
9
+ const licenseKey = process.env.NEW_RELIC_LICENSE_KEY;
10
+ if (!licenseKey) {
11
+ console.warn('[monitor] NEW_RELIC_LICENSE_KEY not set — monitoring disabled');
12
+ return false;
13
+ }
14
+
15
+ // New Relic auto-initializes via require/import — agent is already running
16
+ initialized = true;
17
+ return true;
18
+ }
19
+
20
+ export function captureError(error: Error, context?: ErrorContext): void {
21
+ if (!initialized) return;
22
+
23
+ const customAttributes: Record<string, string> = {};
24
+ if (context?.user) {
25
+ customAttributes['user.id'] = context.user.id;
26
+ if (context.user.email) customAttributes['user.email'] = context.user.email;
27
+ }
28
+ if (context?.tags) {
29
+ Object.assign(customAttributes, context.tags);
30
+ }
31
+
32
+ newrelic.noticeError(error, customAttributes);
33
+ }
34
+
35
+ export function captureMessage(
36
+ message: string,
37
+ level: 'info' | 'warning' | 'error' = 'info',
38
+ ): void {
39
+ if (!initialized) return;
40
+ newrelic.recordCustomEvent('CustomMessage', { message, level });
41
+ }
42
+
43
+ export function setUser(user: { id: string; email?: string }): void {
44
+ if (!initialized) return;
45
+ newrelic.addCustomAttributes({
46
+ 'user.id': user.id,
47
+ ...(user.email ? { 'user.email': user.email } : {}),
48
+ });
49
+ }
50
+
51
+ export async function startSpan<T>(
52
+ name: string,
53
+ op: string,
54
+ callback: () => T | Promise<T>,
55
+ ): Promise<T> {
56
+ if (!initialized) return callback();
57
+ return newrelic.startSegment(`${op}:${name}`, true, callback);
58
+ }
59
+
60
+ export type { MonitorConfig, ErrorContext } from './types.js';
@@ -0,0 +1,12 @@
1
+ export interface MonitorConfig {
2
+ enabled: boolean;
3
+ environment?: string;
4
+ release?: string;
5
+ sampleRate?: number;
6
+ }
7
+
8
+ export interface ErrorContext {
9
+ user?: { id: string; email?: string };
10
+ tags?: Record<string, string>;
11
+ extra?: Record<string, unknown>;
12
+ }
@@ -0,0 +1,70 @@
1
+ import * as Sentry from '@sentry/node';
2
+ import { nodeProfilingIntegration } from '@sentry/profiling-node';
3
+ import type { MonitorConfig, ErrorContext } from './types.js';
4
+
5
+ let initialized = false;
6
+
7
+ export function initMonitoring(config?: Partial<MonitorConfig>): boolean {
8
+ if (initialized) return true;
9
+
10
+ const dsn = process.env.SENTRY_DSN;
11
+ if (!dsn) {
12
+ console.warn('[monitor] SENTRY_DSN not set — monitoring disabled');
13
+ return false;
14
+ }
15
+
16
+ Sentry.init({
17
+ dsn,
18
+ environment: config?.environment ?? process.env.NODE_ENV ?? 'development',
19
+ release: config?.release,
20
+ tracesSampleRate: config?.sampleRate ?? 1.0,
21
+ profilesSampleRate: config?.sampleRate ?? 1.0,
22
+ integrations: [nodeProfilingIntegration()],
23
+ });
24
+
25
+ initialized = true;
26
+ return true;
27
+ }
28
+
29
+ export function captureError(error: Error, context?: ErrorContext): void {
30
+ if (!initialized) return;
31
+
32
+ Sentry.withScope((scope) => {
33
+ if (context?.user) scope.setUser(context.user);
34
+ if (context?.tags) {
35
+ for (const [key, value] of Object.entries(context.tags)) {
36
+ scope.setTag(key, value);
37
+ }
38
+ }
39
+ if (context?.extra) {
40
+ for (const [key, value] of Object.entries(context.extra)) {
41
+ scope.setExtra(key, value);
42
+ }
43
+ }
44
+ Sentry.captureException(error);
45
+ });
46
+ }
47
+
48
+ export function captureMessage(
49
+ message: string,
50
+ level: 'info' | 'warning' | 'error' = 'info',
51
+ ): void {
52
+ if (!initialized) return;
53
+ Sentry.captureMessage(message, level);
54
+ }
55
+
56
+ export function setUser(user: { id: string; email?: string }): void {
57
+ if (!initialized) return;
58
+ Sentry.setUser(user);
59
+ }
60
+
61
+ export async function startSpan<T>(
62
+ name: string,
63
+ op: string,
64
+ callback: () => T | Promise<T>,
65
+ ): Promise<T> {
66
+ if (!initialized) return callback();
67
+ return Sentry.startSpan({ name, op }, callback);
68
+ }
69
+
70
+ export type { MonitorConfig, ErrorContext } from './types.js';
@@ -0,0 +1,12 @@
1
+ export interface MonitorConfig {
2
+ enabled: boolean;
3
+ environment?: string;
4
+ release?: string;
5
+ sampleRate?: number;
6
+ }
7
+
8
+ export interface ErrorContext {
9
+ user?: { id: string; email?: string };
10
+ tags?: Record<string, string>;
11
+ extra?: Record<string, unknown>;
12
+ }
@@ -0,0 +1,56 @@
1
+ import { Queue, Worker, type Job } from 'bullmq';
2
+ import IORedis from 'ioredis';
3
+ import type { JobOptions, JobHandler } from './types.js';
4
+
5
+ const connection = new IORedis(process.env.REDIS_URL || 'redis://localhost:6379', {
6
+ maxRetriesPerRequest: null, // Required by BullMQ
7
+ });
8
+
9
+ const queues = new Map<string, Queue>();
10
+ const workers = new Map<string, Worker>();
11
+
12
+ export function createQueue(name: string): Queue {
13
+ if (queues.has(name)) return queues.get(name)!;
14
+ const queue = new Queue(name, { connection });
15
+ queues.set(name, queue);
16
+ return queue;
17
+ }
18
+
19
+ export function createWorker<T = unknown>(
20
+ name: string,
21
+ handler: JobHandler<T>,
22
+ ): Worker {
23
+ if (workers.has(name)) return workers.get(name)!;
24
+ const worker = new Worker(
25
+ name,
26
+ async (job: Job<T>) => handler(job.data),
27
+ { connection },
28
+ );
29
+ workers.set(name, worker);
30
+ return worker;
31
+ }
32
+
33
+ export async function addJob<T = unknown>(
34
+ queue: Queue,
35
+ name: string,
36
+ data: T,
37
+ options?: JobOptions,
38
+ ): Promise<Job<T>> {
39
+ return queue.add(name, data, {
40
+ delay: options?.delay,
41
+ attempts: options?.attempts ?? 3,
42
+ backoff: options?.backoff ? { type: 'fixed', delay: options.backoff } : undefined,
43
+ priority: options?.priority,
44
+ });
45
+ }
46
+
47
+ export async function closeAll(): Promise<void> {
48
+ await Promise.all([
49
+ ...Array.from(queues.values()).map((q) => q.close()),
50
+ ...Array.from(workers.values()).map((w) => w.close()),
51
+ ]);
52
+ queues.clear();
53
+ workers.clear();
54
+ }
55
+
56
+ export type { JobOptions, JobHandler } from './types.js';
@@ -0,0 +1,10 @@
1
+ export interface JobOptions {
2
+ delay?: number; // ms before job becomes processable
3
+ attempts?: number; // max retry attempts (default: 3)
4
+ backoff?: number; // ms between retries
5
+ priority?: number; // lower = higher priority
6
+ }
7
+
8
+ export interface JobHandler<T = unknown> {
9
+ (data: T): Promise<void>;
10
+ }