@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,79 @@
1
+ import { init, track, identify, groupIdentify as ampGroupIdentify, flush } from '@amplitude/analytics-node';
2
+ import { Identify } from '@amplitude/analytics-node';
3
+ import type { AnalyticsConfig, EventProperties, UserProperties } from './types.js';
4
+
5
+ let initialized = false;
6
+
7
+ export function initAnalytics(config?: Partial<AnalyticsConfig>): boolean {
8
+ const apiKey = config?.apiKey ?? process.env.AMPLITUDE_API_KEY;
9
+ if (!apiKey) {
10
+ console.warn('[analytics] AMPLITUDE_API_KEY not set — analytics disabled');
11
+ return false;
12
+ }
13
+
14
+ init(apiKey, {
15
+ logLevel: config?.debug ? 3 : 0,
16
+ });
17
+
18
+ initialized = true;
19
+
20
+ if (config?.debug) {
21
+ console.log('[analytics] Amplitude initialized');
22
+ }
23
+
24
+ return true;
25
+ }
26
+
27
+ export function trackEvent(
28
+ event: string,
29
+ properties?: EventProperties,
30
+ userId?: string,
31
+ ): void {
32
+ if (!initialized) return;
33
+
34
+ track(event, properties, {
35
+ user_id: userId ?? 'anonymous',
36
+ });
37
+ }
38
+
39
+ export function identifyUser(userId: string, properties?: UserProperties): void {
40
+ if (!initialized) return;
41
+
42
+ const identifyObj = new Identify();
43
+ if (properties) {
44
+ for (const [key, value] of Object.entries(properties)) {
45
+ if (value !== null && value !== undefined) {
46
+ identifyObj.set(key, value);
47
+ }
48
+ }
49
+ }
50
+
51
+ identify(identifyObj, { user_id: userId });
52
+ }
53
+
54
+ export function groupIdentify(
55
+ groupType: string,
56
+ groupId: string,
57
+ properties?: UserProperties,
58
+ ): void {
59
+ if (!initialized) return;
60
+
61
+ const identifyObj = new Identify();
62
+ if (properties) {
63
+ for (const [key, value] of Object.entries(properties)) {
64
+ if (value !== null && value !== undefined) {
65
+ identifyObj.set(key, value);
66
+ }
67
+ }
68
+ }
69
+
70
+ ampGroupIdentify(groupType, groupId, identifyObj);
71
+ }
72
+
73
+ export async function shutdownAnalytics(): Promise<void> {
74
+ if (!initialized) return;
75
+ await flush();
76
+ initialized = false;
77
+ }
78
+
79
+ export type { AnalyticsConfig, EventProperties, UserProperties } from './types.js';
@@ -0,0 +1,12 @@
1
+ export interface AnalyticsConfig {
2
+ apiKey?: string;
3
+ debug?: boolean;
4
+ }
5
+
6
+ export interface EventProperties {
7
+ [key: string]: string | number | boolean | null | undefined;
8
+ }
9
+
10
+ export interface UserProperties {
11
+ [key: string]: string | number | boolean | null | undefined;
12
+ }
@@ -0,0 +1,62 @@
1
+ import Mixpanel from 'mixpanel';
2
+ import type { AnalyticsConfig, EventProperties, UserProperties } from './types.js';
3
+
4
+ let client: Mixpanel.Mixpanel | null = null;
5
+
6
+ export function initAnalytics(config?: Partial<AnalyticsConfig>): boolean {
7
+ const token = config?.apiKey ?? process.env.MIXPANEL_TOKEN;
8
+ if (!token) {
9
+ console.warn('[analytics] MIXPANEL_TOKEN not set — analytics disabled');
10
+ return false;
11
+ }
12
+
13
+ client = Mixpanel.init(token, {
14
+ debug: config?.debug ?? false,
15
+ });
16
+
17
+ if (config?.debug) {
18
+ console.log('[analytics] Mixpanel initialized');
19
+ }
20
+
21
+ return true;
22
+ }
23
+
24
+ export function trackEvent(
25
+ event: string,
26
+ properties?: EventProperties,
27
+ userId?: string,
28
+ ): void {
29
+ if (!client) return;
30
+
31
+ client.track(event, {
32
+ ...properties,
33
+ distinct_id: userId ?? 'anonymous',
34
+ });
35
+ }
36
+
37
+ export function identifyUser(userId: string, properties?: UserProperties): void {
38
+ if (!client) return;
39
+
40
+ client.people.set(userId, properties ?? {});
41
+ }
42
+
43
+ export function groupIdentify(
44
+ groupType: string,
45
+ groupId: string,
46
+ properties?: UserProperties,
47
+ ): void {
48
+ if (!client) return;
49
+
50
+ client.track('$group_identify', {
51
+ $group_type: groupType,
52
+ $group_id: groupId,
53
+ ...properties,
54
+ });
55
+ }
56
+
57
+ export async function shutdownAnalytics(): Promise<void> {
58
+ // Mixpanel Node SDK sends events immediately; no shutdown needed
59
+ client = null;
60
+ }
61
+
62
+ export type { AnalyticsConfig, EventProperties, UserProperties } from './types.js';
@@ -0,0 +1,12 @@
1
+ export interface AnalyticsConfig {
2
+ apiKey?: string;
3
+ debug?: boolean;
4
+ }
5
+
6
+ export interface EventProperties {
7
+ [key: string]: string | number | boolean | null | undefined;
8
+ }
9
+
10
+ export interface UserProperties {
11
+ [key: string]: string | number | boolean | null | undefined;
12
+ }
@@ -0,0 +1,67 @@
1
+ import { PostHog } from 'posthog-node';
2
+ import type { AnalyticsConfig, EventProperties, UserProperties } from './types.js';
3
+
4
+ let client: PostHog | null = null;
5
+
6
+ export function initAnalytics(config?: Partial<AnalyticsConfig>): boolean {
7
+ const apiKey = config?.apiKey ?? process.env.POSTHOG_API_KEY;
8
+ if (!apiKey) {
9
+ console.warn('[analytics] POSTHOG_API_KEY not set — analytics disabled');
10
+ return false;
11
+ }
12
+
13
+ const host = process.env.POSTHOG_HOST ?? 'https://us.i.posthog.com';
14
+
15
+ client = new PostHog(apiKey, { host });
16
+
17
+ if (config?.debug) {
18
+ console.log('[analytics] PostHog initialized');
19
+ }
20
+
21
+ return true;
22
+ }
23
+
24
+ export function trackEvent(
25
+ event: string,
26
+ properties?: EventProperties,
27
+ userId?: string,
28
+ ): void {
29
+ if (!client) return;
30
+
31
+ client.capture({
32
+ distinctId: userId ?? 'anonymous',
33
+ event,
34
+ properties,
35
+ });
36
+ }
37
+
38
+ export function identifyUser(userId: string, properties?: UserProperties): void {
39
+ if (!client) return;
40
+
41
+ client.identify({
42
+ distinctId: userId,
43
+ properties,
44
+ });
45
+ }
46
+
47
+ export function groupIdentify(
48
+ groupType: string,
49
+ groupId: string,
50
+ properties?: UserProperties,
51
+ ): void {
52
+ if (!client) return;
53
+
54
+ client.groupIdentify({
55
+ groupType,
56
+ groupKey: groupId,
57
+ properties,
58
+ });
59
+ }
60
+
61
+ export async function shutdownAnalytics(): Promise<void> {
62
+ if (!client) return;
63
+ await client.shutdown();
64
+ client = null;
65
+ }
66
+
67
+ export type { AnalyticsConfig, EventProperties, UserProperties } from './types.js';
@@ -0,0 +1,12 @@
1
+ export interface AnalyticsConfig {
2
+ apiKey?: string;
3
+ debug?: boolean;
4
+ }
5
+
6
+ export interface EventProperties {
7
+ [key: string]: string | number | boolean | null | undefined;
8
+ }
9
+
10
+ export interface UserProperties {
11
+ [key: string]: string | number | boolean | null | undefined;
12
+ }
@@ -0,0 +1,89 @@
1
+ import { FastifyRequest, FastifyReply } from 'fastify';
2
+
3
+ // Extend FastifyRequest to include user
4
+ declare module 'fastify' {
5
+ interface FastifyRequest {
6
+ user?: {
7
+ id: string;
8
+ email: string;
9
+ name?: string;
10
+ [key: string]: unknown;
11
+ };
12
+ }
13
+ }
14
+
15
+ const AUTHJOY_API_URL = process.env.AUTHJOY_API_URL || 'https://api.authjoy.io';
16
+
17
+ /**
18
+ * Verify a token with AuthJoy
19
+ */
20
+ async function verifyToken(token: string): Promise<Record<string, unknown> | null> {
21
+ try {
22
+ const response = await fetch(`${AUTHJOY_API_URL}/v1/verify`, {
23
+ method: 'POST',
24
+ headers: {
25
+ 'Authorization': `Bearer ${token}`,
26
+ 'Content-Type': 'application/json',
27
+ },
28
+ });
29
+
30
+ if (!response.ok) {
31
+ return null;
32
+ }
33
+
34
+ return await response.json() as Record<string, unknown>;
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Require authentication - returns 401 if not authenticated
42
+ */
43
+ export async function requireAuth(
44
+ request: FastifyRequest,
45
+ reply: FastifyReply
46
+ ): Promise<void> {
47
+ const token = request.headers.authorization?.replace('Bearer ', '');
48
+
49
+ if (!token) {
50
+ reply.status(401).send({ error: 'Unauthorized', message: 'No token provided' });
51
+ return;
52
+ }
53
+
54
+ const user = await verifyToken(token);
55
+
56
+ if (!user) {
57
+ reply.status(401).send({ error: 'Unauthorized', message: 'Invalid token' });
58
+ return;
59
+ }
60
+
61
+ request.user = user as FastifyRequest['user'];
62
+ }
63
+
64
+ /**
65
+ * Optional authentication - attaches user if token is valid, continues otherwise
66
+ */
67
+ export async function optionalAuth(
68
+ request: FastifyRequest,
69
+ _reply: FastifyReply
70
+ ): Promise<void> {
71
+ const token = request.headers.authorization?.replace('Bearer ', '');
72
+
73
+ if (!token) {
74
+ return;
75
+ }
76
+
77
+ const user = await verifyToken(token);
78
+
79
+ if (user) {
80
+ request.user = user as FastifyRequest['user'];
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Get the current user from request (after auth middleware)
86
+ */
87
+ export function getCurrentUser(request: FastifyRequest) {
88
+ return request.user;
89
+ }
@@ -0,0 +1,27 @@
1
+ import { FastifyPluginAsync } from 'fastify';
2
+ import { requireAuth, optionalAuth, getCurrentUser } from '../middleware/auth.js';
3
+
4
+ export const authRoutes: FastifyPluginAsync = async (fastify) => {
5
+ /**
6
+ * Get current authenticated user
7
+ * GET /auth/me
8
+ */
9
+ fastify.get('/me', { preHandler: requireAuth }, async (request, reply) => {
10
+ const user = getCurrentUser(request);
11
+ return reply.send({ user });
12
+ });
13
+
14
+ /**
15
+ * Check authentication status
16
+ * GET /auth/status
17
+ */
18
+ fastify.get('/status', { preHandler: optionalAuth }, async (request, reply) => {
19
+ const user = getCurrentUser(request);
20
+ return reply.send({
21
+ authenticated: !!user,
22
+ user: user || null,
23
+ });
24
+ });
25
+ };
26
+
27
+ export default authRoutes;
@@ -0,0 +1,40 @@
1
+ import { AuthJoyProvider } from '@authjoyio/react';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface AuthProviderProps {
5
+ children: ReactNode;
6
+ }
7
+
8
+ /**
9
+ * AuthJoy authentication provider wrapper
10
+ *
11
+ * Wraps your app with AuthJoy context for authentication state management.
12
+ *
13
+ * Required environment variables:
14
+ * - VITE_AUTHJOY_CLIENT_ID: Your AuthJoy client ID
15
+ * - VITE_AUTHJOY_DOMAIN: Your AuthJoy domain (e.g., your-app.authjoy.io)
16
+ */
17
+ export function AuthProvider({ children }: AuthProviderProps) {
18
+ const clientId = import.meta.env.VITE_AUTHJOY_CLIENT_ID;
19
+ const domain = import.meta.env.VITE_AUTHJOY_DOMAIN;
20
+
21
+ if (!clientId || !domain) {
22
+ console.warn(
23
+ 'AuthJoy not configured. Set VITE_AUTHJOY_CLIENT_ID and VITE_AUTHJOY_DOMAIN environment variables.'
24
+ );
25
+ }
26
+
27
+ return (
28
+ <AuthJoyProvider
29
+ config={{
30
+ clientId: clientId || '',
31
+ domain: domain || '',
32
+ redirectUri: window.location.origin,
33
+ }}
34
+ >
35
+ {children}
36
+ </AuthJoyProvider>
37
+ );
38
+ }
39
+
40
+ export default AuthProvider;
@@ -0,0 +1,71 @@
1
+ import { useAuthJoy } from '@authjoyio/react';
2
+
3
+ /**
4
+ * Authentication hook for accessing user state and auth actions
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * function ProfileButton() {
9
+ * const { user, isAuthenticated, login, logout } = useAuth();
10
+ *
11
+ * if (!isAuthenticated) {
12
+ * return <button onClick={() => login()}>Sign In</button>;
13
+ * }
14
+ *
15
+ * return (
16
+ * <div>
17
+ * <span>{user?.name}</span>
18
+ * <button onClick={() => logout()}>Sign Out</button>
19
+ * </div>
20
+ * );
21
+ * }
22
+ * ```
23
+ */
24
+ export function useAuth() {
25
+ const authJoy = useAuthJoy();
26
+
27
+ return {
28
+ /**
29
+ * The authenticated user object, or null if not authenticated
30
+ */
31
+ user: authJoy.user,
32
+
33
+ /**
34
+ * Whether authentication state is still loading
35
+ */
36
+ isLoading: authJoy.isLoading,
37
+
38
+ /**
39
+ * Whether the user is authenticated
40
+ */
41
+ isAuthenticated: authJoy.isAuthenticated,
42
+
43
+ /**
44
+ * Trigger the login flow
45
+ * @param options Optional login options (e.g., { screen_hint: 'signup' })
46
+ */
47
+ login: authJoy.login,
48
+
49
+ /**
50
+ * Log out the current user
51
+ * @param options Optional logout options (e.g., { returnTo: '/goodbye' })
52
+ */
53
+ logout: authJoy.logout,
54
+
55
+ /**
56
+ * Get an access token for API calls
57
+ * @returns Promise<string> The access token
58
+ */
59
+ getToken: authJoy.getToken,
60
+
61
+ /**
62
+ * Check if the user has a specific permission/scope
63
+ * @param permission The permission to check
64
+ */
65
+ hasPermission: (permission: string) => {
66
+ return authJoy.user?.permissions?.includes(permission) ?? false;
67
+ },
68
+ };
69
+ }
70
+
71
+ export default useAuth;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * AuthJoy configuration and utilities
3
+ */
4
+
5
+ export const authConfig = {
6
+ clientId: import.meta.env.VITE_AUTHJOY_CLIENT_ID || '',
7
+ domain: import.meta.env.VITE_AUTHJOY_DOMAIN || '',
8
+ redirectUri: typeof window !== 'undefined' ? window.location.origin : '',
9
+ };
10
+
11
+ /**
12
+ * Make an authenticated API request
13
+ *
14
+ * @param url The URL to fetch
15
+ * @param options Fetch options
16
+ * @param getToken Function to get the current access token
17
+ * @returns The fetch response
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const { getToken } = useAuth();
22
+ * const response = await authFetch('/api/users', { method: 'GET' }, getToken);
23
+ * ```
24
+ */
25
+ export async function authFetch(
26
+ url: string,
27
+ options: RequestInit = {},
28
+ getToken: () => Promise<string>
29
+ ): Promise<Response> {
30
+ const token = await getToken();
31
+
32
+ return fetch(url, {
33
+ ...options,
34
+ headers: {
35
+ ...options.headers,
36
+ Authorization: `Bearer ${token}`,
37
+ 'Content-Type': 'application/json',
38
+ },
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Create an authenticated fetch wrapper
44
+ *
45
+ * @param getToken Function to get the current access token
46
+ * @returns A fetch function that automatically adds auth headers
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const { getToken } = useAuth();
51
+ * const api = createAuthFetch(getToken);
52
+ * const response = await api('/api/users');
53
+ * ```
54
+ */
55
+ export function createAuthFetch(getToken: () => Promise<string>) {
56
+ return async (url: string, options: RequestInit = {}): Promise<Response> => {
57
+ return authFetch(url, options, getToken);
58
+ };
59
+ }
@@ -0,0 +1,84 @@
1
+ 'use client';
2
+
3
+ import { useAuth } from '../hooks/use-auth';
4
+ import {
5
+ Card,
6
+ CardHeader,
7
+ CardTitle,
8
+ CardDescription,
9
+ CardContent,
10
+ } from '@primstack/ui/card';
11
+ import { Avatar, AvatarImage, AvatarFallback } from '@primstack/ui/avatar';
12
+ import { Badge } from '@primstack/ui/badge';
13
+ import { Button } from '@primstack/ui/button';
14
+ import { Separator } from '@primstack/ui/separator';
15
+ import { useEffect } from 'react';
16
+
17
+ function getInitials(name?: string): string {
18
+ if (!name) return '?';
19
+ return name
20
+ .split(' ')
21
+ .map((part) => part[0])
22
+ .join('')
23
+ .toUpperCase()
24
+ .slice(0, 2);
25
+ }
26
+
27
+ export default function AccountPage() {
28
+ const { user, isAuthenticated, isLoading, logout } = useAuth();
29
+
30
+ useEffect(() => {
31
+ if (!isLoading && !isAuthenticated) {
32
+ window.location.href = '/login';
33
+ }
34
+ }, [isLoading, isAuthenticated]);
35
+
36
+ if (isLoading || !isAuthenticated) {
37
+ return null;
38
+ }
39
+
40
+ return (
41
+ <div className="flex min-h-screen items-center justify-center p-4">
42
+ <Card className="w-full max-w-md">
43
+ <CardHeader className="items-center text-center">
44
+ <Avatar className="h-20 w-20">
45
+ <AvatarImage src={user?.picture} alt={user?.name} />
46
+ <AvatarFallback className="text-lg">
47
+ {getInitials(user?.name)}
48
+ </AvatarFallback>
49
+ </Avatar>
50
+ <CardTitle className="mt-4">{user?.name}</CardTitle>
51
+ <CardDescription>{user?.email}</CardDescription>
52
+ <Badge variant="outline" className="mt-2">
53
+ Authenticated
54
+ </Badge>
55
+ </CardHeader>
56
+
57
+ <Separator />
58
+
59
+ <CardContent className="space-y-4 pt-6">
60
+ <div className="space-y-1">
61
+ <p className="text-sm font-medium text-muted-foreground">Email</p>
62
+ <p className="text-sm">{user?.email}</p>
63
+ </div>
64
+ {user?.name && (
65
+ <div className="space-y-1">
66
+ <p className="text-sm font-medium text-muted-foreground">Name</p>
67
+ <p className="text-sm">{user.name}</p>
68
+ </div>
69
+ )}
70
+
71
+ <Separator />
72
+
73
+ <Button
74
+ variant="destructive"
75
+ className="w-full"
76
+ onClick={() => logout({ returnTo: window.location.origin })}
77
+ >
78
+ Sign Out
79
+ </Button>
80
+ </CardContent>
81
+ </Card>
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,73 @@
1
+ 'use client';
2
+
3
+ import { useAuth } from '../hooks/use-auth';
4
+ import {
5
+ Card,
6
+ CardHeader,
7
+ CardTitle,
8
+ CardDescription,
9
+ CardContent,
10
+ CardFooter,
11
+ } from '@primstack/ui/card';
12
+ import { Button } from '@primstack/ui/button';
13
+ import { Separator } from '@primstack/ui/separator';
14
+ import { useEffect } from 'react';
15
+
16
+ export default function LoginPage() {
17
+ const { login, isAuthenticated, isLoading } = useAuth();
18
+
19
+ useEffect(() => {
20
+ if (isAuthenticated) {
21
+ window.location.href = '/';
22
+ }
23
+ }, [isAuthenticated]);
24
+
25
+ if (isLoading || isAuthenticated) {
26
+ return null;
27
+ }
28
+
29
+ return (
30
+ <div className="flex min-h-screen items-center justify-center p-4">
31
+ <Card className="w-full max-w-md">
32
+ <CardHeader className="text-center">
33
+ <CardTitle className="text-2xl">Welcome back</CardTitle>
34
+ <CardDescription>
35
+ Sign in to your account to continue.
36
+ </CardDescription>
37
+ </CardHeader>
38
+
39
+ <CardContent className="space-y-4">
40
+ <Button
41
+ className="w-full"
42
+ size="lg"
43
+ onClick={() => login()}
44
+ >
45
+ Sign In
46
+ </Button>
47
+
48
+ <div className="relative">
49
+ <Separator />
50
+ <span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-card px-2 text-xs text-muted-foreground">
51
+ or
52
+ </span>
53
+ </div>
54
+
55
+ <Button
56
+ className="w-full"
57
+ variant="outline"
58
+ size="lg"
59
+ onClick={() => login({ screen_hint: 'signup' })}
60
+ >
61
+ Create Account
62
+ </Button>
63
+ </CardContent>
64
+
65
+ <CardFooter className="justify-center">
66
+ <p className="text-xs text-muted-foreground">
67
+ Secured by AuthJoy
68
+ </p>
69
+ </CardFooter>
70
+ </Card>
71
+ </div>
72
+ );
73
+ }