anhedral 0.1.1 → 0.1.3

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.
@@ -1,9 +1,11 @@
1
1
  import path from 'node:path';
2
2
  import { writeFile, exec } from '../util.js';
3
+ import { anhedralPrint } from '../print.js';
3
4
  import { getBackendInstallCommands } from '../commands.js';
4
5
  export async function scaffoldBackend(root, { projectName, displayName, frontendUrl }) {
5
- const dir = path.join(root, 'backend');
6
- // writeFile creates parent dirs automatically via mkdirSync
6
+ const dir = path.join(root, 'apps/api');
7
+ anhedralPrint.section('Backend (Fastify)');
8
+ anhedralPrint.step('Writing backend source files');
7
9
  writePackageJson(dir, projectName);
8
10
  writeTsConfig(dir);
9
11
  writeEslintConfig(dir);
@@ -12,7 +14,6 @@ export async function scaffoldBackend(root, { projectName, displayName, frontend
12
14
  writeEnvFile(dir, frontendUrl);
13
15
  writeDrizzleConfig(dir);
14
16
  writeVitestConfig(dir);
15
- writeVercelJson(dir);
16
17
  writeTypes(dir);
17
18
  writeDbFiles(dir);
18
19
  writeErrorFiles(dir);
@@ -24,9 +25,16 @@ export async function scaffoldBackend(root, { projectName, displayName, frontend
24
25
  writeAppAndIndex(dir, displayName);
25
26
  writeVercelEntry(dir);
26
27
  writeTestFiles(dir);
28
+ anhedralPrint.done('Backend source files written');
29
+ if (process.env.ANHEDRAL_SKIP_INSTALL === '1') {
30
+ anhedralPrint.info('Skipping backend dependency install (ANHEDRAL_SKIP_INSTALL=1)');
31
+ return;
32
+ }
33
+ anhedralPrint.step('Installing backend dependencies');
27
34
  for (const command of getBackendInstallCommands()) {
28
35
  exec(command.cmd, dir);
29
36
  }
37
+ anhedralPrint.done('Backend dependencies installed');
30
38
  }
31
39
  // ═══════════════════════════════════════════════════════════════════════════
32
40
  // Config files
@@ -42,7 +50,7 @@ function writePackageJson(dir, projectName) {
42
50
  build: 'pnpm typecheck',
43
51
  typecheck: 'tsc --noEmit',
44
52
  'db:generate': 'drizzle-kit generate',
45
- 'db:migrate': 'pnpm tsx --env-file=.env src/db/migrate.ts',
53
+ 'db:migrate': 'pnpm --filter @anhedral/db db:migrate',
46
54
  'db:studio': 'drizzle-kit studio',
47
55
  'db:push': 'drizzle-kit push',
48
56
  lint: 'eslint . --ext .js,.ts',
@@ -51,14 +59,17 @@ function writePackageJson(dir, projectName) {
51
59
  },
52
60
  keywords: [],
53
61
  license: 'MIT',
62
+ dependencies: {
63
+ '@anhedral/db': 'workspace:*',
64
+ },
54
65
  }, null, 2) + '\n');
55
66
  }
56
67
  function writeTsConfig(dir) {
57
68
  writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify({
58
69
  compilerOptions: {
59
70
  target: 'ESNext',
60
- module: 'NodeNext',
61
- moduleResolution: 'NodeNext',
71
+ module: 'ESNext',
72
+ moduleResolution: 'Bundler',
62
73
  skipLibCheck: true,
63
74
  rootDir: '.',
64
75
  outDir: 'dist',
@@ -104,6 +115,7 @@ function writeEnvExample(dir, frontendUrl = 'http://localhost:8081') {
104
115
  PORT=8787
105
116
  NODE_ENV=development
106
117
  LOG_LEVEL=info
118
+ ANHEDRAL_DEMO=false
107
119
 
108
120
  # Database (NeonDB)
109
121
  DATABASE_URL="postgresql://neondb_owner:***@***.neon.tech/neondb?sslmode=require"
@@ -134,6 +146,7 @@ function writeEnvFile(dir, frontendUrl = 'http://localhost:8081') {
134
146
  PORT=8787
135
147
  NODE_ENV=development
136
148
  LOG_LEVEL=info
149
+ ANHEDRAL_DEMO=true
137
150
 
138
151
  # Database (NeonDB)
139
152
  DATABASE_URL="postgresql://user:pass@localhost:5432/app?sslmode=disable"
@@ -162,8 +175,8 @@ RC_OFFERING_ID=default
162
175
  function writeDrizzleConfig(dir) {
163
176
  writeFile(path.join(dir, 'drizzle.config.ts'), `import type { Config } from 'drizzle-kit';
164
177
  export default {
165
- schema: './src/db/schema.ts',
166
- out: './src/db/migrations',
178
+ schema: '../../packages/db/src/schema.ts',
179
+ out: '../../packages/db/migrations',
167
180
  dialect: 'postgresql',
168
181
  dbCredentials: { url: process.env.DATABASE_URL! }
169
182
  } satisfies Config;
@@ -184,13 +197,6 @@ export default defineConfig({
184
197
  });
185
198
  `);
186
199
  }
187
- function writeVercelJson(dir) {
188
- writeFile(path.join(dir, 'vercel.json'), JSON.stringify({
189
- version: 2,
190
- builds: [{ src: 'api/index.ts', use: '@vercel/node' }],
191
- routes: [{ src: '/(.*)', dest: 'api/index.ts' }],
192
- }, null, 2) + '\n');
193
- }
194
200
  // ═══════════════════════════════════════════════════════════════════════════
195
201
  // Types
196
202
  // ═══════════════════════════════════════════════════════════════════════════
@@ -207,6 +213,7 @@ export interface AppEnv {
207
213
  PORT: number;
208
214
  NODE_ENV: string;
209
215
  LOG_LEVEL: string;
216
+ ANHEDRAL_DEMO?: string | null;
210
217
  CLERK_PUBLISHABLE_KEY?: string | null;
211
218
  CLERK_SECRET_KEY?: string | null;
212
219
  FRONTEND_URL?: string | null;
@@ -265,183 +272,12 @@ export {};
265
272
  // Database
266
273
  // ═══════════════════════════════════════════════════════════════════════════
267
274
  function writeDbFiles(dir) {
268
- writeFile(path.join(dir, 'src/db/index.ts'), `import { drizzle } from 'drizzle-orm/neon-http';
269
- import { neon } from '@neondatabase/serverless';
270
-
271
- if (!process.env.DATABASE_URL) {
272
- throw new Error('DATABASE_URL is not defined');
273
- }
274
-
275
- const sql = neon(process.env.DATABASE_URL);
276
- export const db = drizzle(sql);
277
- export type Database = typeof db;
275
+ writeFile(path.join(dir, 'src/db/index.ts'), `export { db } from '@anhedral/db';
276
+ export type { Database } from '@anhedral/db';
278
277
  `);
279
- writeFile(path.join(dir, 'src/db/schema.ts'), `import { pgTable, text, timestamp, boolean, integer, index, jsonb } from 'drizzle-orm/pg-core';
280
- import type { InferSelectModel, InferInsertModel } from 'drizzle-orm';
281
-
282
- export const SUBSCRIPTION_TIERS = ['free', 'pro'] as const;
283
- export type SubscriptionTier = (typeof SUBSCRIPTION_TIERS)[number];
284
-
285
- export const SUBSCRIPTION_STATUSES = ['active', 'expired', 'canceled'] as const;
286
- export type SubscriptionStatus = (typeof SUBSCRIPTION_STATUSES)[number];
287
-
288
- export const SUBSCRIPTION_METHODS = ['trialing', 'redeemed', 'paid'] as const;
289
- export type SubscriptionMethod = (typeof SUBSCRIPTION_METHODS)[number];
290
-
291
- export const SUBSCRIPTION_ORIGINS = ['web', 'apple', 'google'] as const;
292
- export type SubscriptionOrigin = (typeof SUBSCRIPTION_ORIGINS)[number];
293
-
294
- export const SUBSCRIPTION_EVENT_TYPES = [
295
- 'trial_started', 'trial_converted', 'trial_expired',
296
- 'initial_purchase', 'renewal', 'product_change',
297
- 'cancellation_scheduled', 'cancellation_unscheduled', 'subscription_expired', 'subscription_canceled',
298
- 'promo_redeemed', 'billing_issue', 'billing_recovered',
299
- ] as const;
300
- export type SubscriptionEventType = (typeof SUBSCRIPTION_EVENT_TYPES)[number];
301
-
302
- export type SubscriptionMetadata = {
303
- revenueCatProductId?: string; lastWebhookUpdate?: string; cancelReason?: string;
304
- redeemCode?: string; redeemCodeRedeemedAt?: string;
305
- };
306
-
307
- export type SubscriptionEventMetadata = {
308
- revenueCatEventType?: string; revenueCatProductId?: string; promoCode?: string;
309
- billingPeriod?: string; price?: { amount: number; currency: string };
310
- store?: string; transactionId?: string; reason?: string;
311
- };
312
-
313
- export const PROMO_CODE_DURATIONS = [1, 6, 12] as const;
314
- export type PromoCodeDuration = (typeof PROMO_CODE_DURATIONS)[number];
315
-
316
- export const users = pgTable('users', {
317
- id: text('id').primaryKey(),
318
- email: text('email').notNull().unique(),
319
- displayName: text('display_name'),
320
- lastLoginAt: timestamp('last_login_at'),
321
- profileImageUrl: text('profile_image_url'),
322
- avatarObjectKey: text('avatar_object_key'),
323
- avatarMimeType: text('avatar_mime_type'),
324
- creditsBalance: integer('credits_balance').notNull().default(250),
325
- createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
326
- });
327
-
328
- export const subscriptions = pgTable('subscriptions', {
329
- id: text('id').primaryKey(),
330
- userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }).unique(),
331
- tier: text('tier').$type<SubscriptionTier>().notNull().default('free'),
332
- status: text('status').$type<SubscriptionStatus>().notNull().default('active'),
333
- method: text('method').$type<SubscriptionMethod>(),
334
- origin: text('origin').$type<SubscriptionOrigin>(),
335
- billingPeriod: text('billing_period'),
336
- currentPeriodStart: timestamp('current_period_start'),
337
- currentPeriodEnd: timestamp('current_period_end'),
338
- canceledAt: timestamp('canceled_at'),
339
- cancelAtPeriodEnd: boolean('cancel_at_period_end').notNull().default(false),
340
- trialStart: timestamp('trial_start'),
341
- trialEnd: timestamp('trial_end'),
342
- dailyLimit: integer('daily_limit'),
343
- metadata: jsonb('metadata').$type<SubscriptionMetadata>(),
344
- createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
345
- updatedAt: timestamp('updated_at').$defaultFn(() => new Date()).notNull(),
346
- }, (t) => [
347
- index('subscriptions_user_idx').on(t.userId),
348
- index('subscriptions_status_idx').on(t.status),
349
- index('subscriptions_tier_idx').on(t.tier),
350
- index('subscriptions_period_end_idx').on(t.currentPeriodEnd),
351
- ]);
352
-
353
- export const subscriptionEvents = pgTable('subscription_events', {
354
- id: text('id').primaryKey(),
355
- userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
356
- subscriptionId: text('subscription_id').references(() => subscriptions.id, { onDelete: 'set null' }),
357
- eventType: text('event_type').$type<SubscriptionEventType>().notNull(),
358
- previousTier: text('previous_tier').$type<SubscriptionTier>(),
359
- previousStatus: text('previous_status').$type<SubscriptionStatus>(),
360
- previousMethod: text('previous_method').$type<SubscriptionMethod>(),
361
- newTier: text('new_tier').$type<SubscriptionTier>(),
362
- newStatus: text('new_status').$type<SubscriptionStatus>(),
363
- newMethod: text('new_method').$type<SubscriptionMethod>(),
364
- revenueCatEventType: text('revenuecat_event_type'),
365
- revenueCatProductId: text('revenuecat_product_id'),
366
- origin: text('origin').$type<SubscriptionOrigin>(),
367
- periodStart: timestamp('period_start'),
368
- periodEnd: timestamp('period_end'),
369
- metadata: jsonb('metadata').$type<SubscriptionEventMetadata>(),
370
- createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
371
- }, (t) => [
372
- index('sub_events_user_idx').on(t.userId),
373
- index('sub_events_user_created_idx').on(t.userId, t.createdAt),
374
- index('sub_events_type_idx').on(t.eventType),
375
- ]);
376
-
377
- export const trialClaims = pgTable('trial_claims', {
378
- id: text('id').primaryKey(),
379
- userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
380
- email: text('email').notNull().unique(),
381
- claimedAt: timestamp('claimed_at').$defaultFn(() => new Date()).notNull(),
382
- }, (t) => [index('trial_claims_email_idx').on(t.email)]);
383
-
384
- export const promoCodes = pgTable('promo_codes', {
385
- id: text('id').primaryKey(),
386
- code: text('code').notNull().unique(),
387
- months: integer('months').$type<PromoCodeDuration>().notNull(),
388
- maxRedemptions: integer('max_redemptions').notNull().default(1),
389
- redeemedCount: integer('redeemed_count').notNull().default(0),
390
- expiresAt: timestamp('expires_at'),
391
- createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
392
- }, (t) => [index('promo_codes_code_idx').on(t.code)]);
393
-
394
- export const promoRedemptions = pgTable('promo_redemptions', {
395
- id: text('id').primaryKey(),
396
- promoCodeId: text('promo_code_id').notNull().references(() => promoCodes.id, { onDelete: 'cascade' }),
397
- userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
398
- redeemedAt: timestamp('redeemed_at').$defaultFn(() => new Date()).notNull(),
399
- entitlementExpiresAt: timestamp('entitlement_expires_at').notNull(),
400
- }, (t) => [
401
- index('promo_redemptions_user_idx').on(t.userId),
402
- index('promo_redemptions_code_idx').on(t.promoCodeId),
403
- ]);
404
-
405
- export type Users = InferSelectModel<typeof users>;
406
- export type NewUsers = InferInsertModel<typeof users>;
407
- export type Subscriptions = InferSelectModel<typeof subscriptions>;
408
- export type NewSubscriptions = InferInsertModel<typeof subscriptions>;
409
- export type SubscriptionEvents = InferSelectModel<typeof subscriptionEvents>;
410
- export type NewSubscriptionEvents = InferInsertModel<typeof subscriptionEvents>;
411
- export type TrialClaims = InferSelectModel<typeof trialClaims>;
412
- export type NewTrialClaims = InferInsertModel<typeof trialClaims>;
413
- export type PromoCodes = InferSelectModel<typeof promoCodes>;
414
- export type NewPromoCodes = InferInsertModel<typeof promoCodes>;
415
- export type PromoRedemptions = InferSelectModel<typeof promoRedemptions>;
416
- export type NewPromoRedemptions = InferInsertModel<typeof promoRedemptions>;
278
+ writeFile(path.join(dir, 'src/db/schema.ts'), `export * from '@anhedral/db/schema';
417
279
  `);
418
- writeFile(path.join(dir, 'src/db/migrate.ts'), `import { drizzle } from 'drizzle-orm/neon-http';
419
- import { migrate } from 'drizzle-orm/neon-http/migrator';
420
- import { neon } from '@neondatabase/serverless';
421
- import dotenv from 'dotenv';
422
-
423
- dotenv.config({ quiet: true });
424
-
425
- const runMigrate = async () => {
426
- if (!process.env.DATABASE_URL) {
427
- throw new Error('DATABASE_URL is not defined');
428
- }
429
-
430
- const client = neon(process.env.DATABASE_URL);
431
- const db = drizzle(client);
432
-
433
- console.log('⏳ Running migrations...');
434
- const start = Date.now();
435
- await migrate(db, { migrationsFolder: './src/db/migrations' });
436
- console.log('✅ Migrations completed in', Date.now() - start, 'ms');
437
- process.exit(0);
438
- };
439
-
440
- runMigrate().catch((err) => {
441
- console.error('❌ Migration failed');
442
- console.error(err);
443
- process.exit(1);
444
- });
280
+ writeFile(path.join(dir, 'src/db/migrate.ts'), `import '@anhedral/db/migrate';
445
281
  `);
446
282
  }
447
283
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1297,6 +1133,7 @@ const configPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {
1297
1133
  PORT: { type: 'number' },
1298
1134
  NODE_ENV: { type: 'string', default: 'development' },
1299
1135
  LOG_LEVEL: { type: 'string', default: 'info' },
1136
+ ANHEDRAL_DEMO: { type: 'string', default: 'false' },
1300
1137
  CLERK_PUBLISHABLE_KEY: { type: 'string', nullable: true },
1301
1138
  CLERK_SECRET_KEY: { type: 'string', nullable: true },
1302
1139
  FRONTEND_URL: { type: 'string', nullable: true },
@@ -1366,6 +1203,15 @@ export const clerkAuthPlugin: FastifyPluginAsync = async (fastify: FastifyInstan
1366
1203
  const authenticate = async (req: FastifyRequest, _reply: FastifyReply) => {
1367
1204
  if (req.method === 'OPTIONS' || req.url.startsWith('/health')) return;
1368
1205
 
1206
+ if (fastify.env?.ANHEDRAL_DEMO === 'true') {
1207
+ req.user = {
1208
+ id: 'user_demo',
1209
+ subscriptionTier: 'pro',
1210
+ subscriptionStatus: 'active',
1211
+ };
1212
+ return;
1213
+ }
1214
+
1369
1215
  const authHeader = req.headers.authorization;
1370
1216
  if (!authHeader?.startsWith('Bearer ')) throw AuthError.unauthorized();
1371
1217
 
@@ -1929,6 +1775,23 @@ const authRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => {
1929
1775
  }, async (req: FastifyRequest, reply: FastifyReply) => {
1930
1776
  if (!req.user?.id) throw AuthError.unauthorized();
1931
1777
 
1778
+ if (fastify.env.ANHEDRAL_DEMO === 'true') {
1779
+ return reply.send({
1780
+ user: {
1781
+ id: 'user_demo',
1782
+ email: 'demo@anhedral.dev',
1783
+ firstName: 'Demo',
1784
+ lastName: 'Builder',
1785
+ displayName: '${displayName} Demo',
1786
+ imageUrl: null,
1787
+ avatarUrl: null,
1788
+ creditsBalance: 250,
1789
+ subscriptionTier: 'pro',
1790
+ subscriptionStatus: 'active',
1791
+ },
1792
+ });
1793
+ }
1794
+
1932
1795
  const clerkUser = await clerkClient.users.getUser(req.user.id);
1933
1796
  const userData = await fastify.repos.users.getDashboardProfile(req.user.id);
1934
1797
  const primaryEmail = clerkUser.emailAddresses.find(
@@ -2048,6 +1911,19 @@ const subscriptionRoutes: FastifyPluginAsync = async (fastify: FastifyInstance)
2048
1911
  }, async (req, reply) => {
2049
1912
  const userId = req.user?.id;
2050
1913
  if (!userId) throw AuthError.unauthorized();
1914
+ if (app.env.ANHEDRAL_DEMO === 'true') {
1915
+ reply.header('Cache-Control', 'private, no-store');
1916
+ return reply.send({
1917
+ pro: true,
1918
+ inTrial: false,
1919
+ expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
1920
+ periodStart: new Date().toISOString(),
1921
+ periodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
1922
+ method: 'paid',
1923
+ managementUrl: 'https://app.revenuecat.com/',
1924
+ cancelAtPeriodEnd: false,
1925
+ });
1926
+ }
2051
1927
  const refreshRaw = (req.query as unknown as { refresh?: unknown }).refresh;
2052
1928
  const requestedRefresh = refreshRaw === true || refreshRaw === 'true' || refreshRaw === 1 || refreshRaw === '1';
2053
1929
  const forceRefresh = Boolean(app.env.RC_SECRET_API_KEY) && requestedRefresh;
@@ -2061,6 +1937,9 @@ const subscriptionRoutes: FastifyPluginAsync = async (fastify: FastifyInstance)
2061
1937
  }, async (req, reply) => {
2062
1938
  const userId = req.user?.id;
2063
1939
  if (!userId) throw AuthError.unauthorized();
1940
+ if (app.env.ANHEDRAL_DEMO === 'true') {
1941
+ return { ok: true, expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() };
1942
+ }
2064
1943
  const { code } = req.body;
2065
1944
  const validation = await app.repos.promoCodes.validateCode(code, userId);
2066
1945
  if (!validation.valid || !validation.promoCode) {
@@ -2299,6 +2178,13 @@ async function getApp(): Promise<FastifyInstance> {
2299
2178
 
2300
2179
  export default async function handler(req: VercelRequest, res: VercelResponse) {
2301
2180
  const app = await getApp();
2181
+ if (req.url === '/api' || req.url === '/backend') {
2182
+ req.url = '/';
2183
+ } else if (req.url?.startsWith('/api/')) {
2184
+ req.url = req.url.slice('/api'.length);
2185
+ } else if (req.url?.startsWith('/backend/')) {
2186
+ req.url = req.url.slice('/backend'.length);
2187
+ }
2302
2188
  app.server.emit('request', req, res);
2303
2189
  }
2304
2190
  `);
@@ -785,7 +785,7 @@ export async function POST(request: Request) {
785
785
  return Response.json({ error: 'No email address found for the signed-in user.' }, { status: 400 });
786
786
  }
787
787
 
788
- await syncUserProfile({
788
+ const user = await syncUserProfile({
789
789
  clerkUserId: userId,
790
790
  email,
791
791
  displayName,
@@ -793,6 +793,21 @@ export async function POST(request: Request) {
793
793
 
794
794
  const origin = new URL(request.url).origin;
795
795
  const stripe = getStripe();
796
+ let stripeCustomerId = user?.stripeCustomerId ?? null;
797
+
798
+ if (!stripeCustomerId) {
799
+ const customer = await stripe.customers.create({
800
+ email,
801
+ name: displayName,
802
+ metadata: {
803
+ clerkUserId: userId,
804
+ },
805
+ });
806
+
807
+ stripeCustomerId = customer.id;
808
+ await updateStripeCustomerForUser(userId, stripeCustomerId);
809
+ }
810
+
796
811
  const session = await stripe.checkout.sessions.create({
797
812
  mode: 'subscription',
798
813
  line_items: [
@@ -802,7 +817,7 @@ export async function POST(request: Request) {
802
817
  },
803
818
  ],
804
819
  allow_promotion_codes: true,
805
- customer_email: email,
820
+ customer: stripeCustomerId,
806
821
  client_reference_id: userId,
807
822
  success_url: \`\${origin}/dashboard?checkout=success\`,
808
823
  cancel_url: \`\${origin}/dashboard?checkout=cancelled\`,
@@ -811,10 +826,6 @@ export async function POST(request: Request) {
811
826
  },
812
827
  });
813
828
 
814
- if (typeof session.customer === 'string' && session.customer.length > 0) {
815
- await updateStripeCustomerForUser(userId, session.customer);
816
- }
817
-
818
829
  return Response.json({ url: session.url });
819
830
  }
820
831
  `);
@@ -857,10 +868,15 @@ export async function POST(request: Request) {
857
868
  import { HeaderUserMenu } from '@/components/dashboard/header-user-menu';
858
869
  import { getClerkProfile } from '@/lib/auth/clerk';
859
870
  import { getDashboardState } from '@/lib/app/dashboard';
871
+ import { clerkEnvReady } from '@/lib/demo-env';
860
872
  import { buttonVariants } from '@/lib/ui/button';
861
873
  import { cn } from '@/lib/utils';
862
874
 
863
875
  export default async function DashboardPage() {
876
+ if (!clerkEnvReady) {
877
+ redirect('/');
878
+ }
879
+
864
880
  const { userId } = await auth();
865
881
 
866
882
  if (!userId) {
@@ -1543,25 +1559,9 @@ function hasRealValue(value: string | undefined) {
1543
1559
  return !value.includes('***') && !value.includes('demo_placeholder');
1544
1560
  }
1545
1561
 
1546
- function isValidPublishableKey(value: string | undefined) {
1547
- if (!hasRealValue(value)) {
1548
- return false;
1549
- }
1550
-
1551
- return /^pk_(test|live)_[A-Za-z0-9]+$/.test(value ?? '');
1552
- }
1553
-
1554
- function isValidSecretKey(value: string | undefined) {
1555
- if (!hasRealValue(value)) {
1556
- return false;
1557
- }
1558
-
1559
- return /^sk_(test|live)_[A-Za-z0-9]+$/.test(value ?? '');
1560
- }
1561
-
1562
1562
  export const clerkEnvReady =
1563
- isValidPublishableKey(process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY) &&
1564
- isValidSecretKey(process.env.CLERK_SECRET_KEY);
1563
+ hasRealValue(process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY) &&
1564
+ hasRealValue(process.env.CLERK_SECRET_KEY);
1565
1565
 
1566
1566
  export const requiredEnvGroups = REQUIRED_ENV_GROUPS;
1567
1567
  `);
@@ -1746,9 +1746,15 @@ export default async function HomePage() {
1746
1746
  );
1747
1747
  }
1748
1748
  `);
1749
- writeFile(path.join(root, 'app/sign-in/[[...sign-in]]/page.tsx'), `import { SignInPanel } from '@/components/auth/sign-in-panel';
1749
+ writeFile(path.join(root, 'app/sign-in/[[...sign-in]]/page.tsx'), `import { redirect } from 'next/navigation';
1750
+ import { SignInPanel } from '@/components/auth/sign-in-panel';
1751
+ import { clerkEnvReady } from '@/lib/demo-env';
1750
1752
 
1751
1753
  export default function SignInPage() {
1754
+ if (!clerkEnvReady) {
1755
+ redirect('/');
1756
+ }
1757
+
1752
1758
  return (
1753
1759
  <main className="min-h-screen bg-background">
1754
1760
  <section className="container mx-auto flex min-h-screen max-w-5xl items-center justify-center px-4 py-8 sm:px-6">
@@ -1758,9 +1764,15 @@ export default function SignInPage() {
1758
1764
  );
1759
1765
  }
1760
1766
  `);
1761
- writeFile(path.join(root, 'app/sign-up/[[...sign-up]]/page.tsx'), `import { SignUpPanel } from '@/components/auth/sign-up-panel';
1767
+ writeFile(path.join(root, 'app/sign-up/[[...sign-up]]/page.tsx'), `import { redirect } from 'next/navigation';
1768
+ import { SignUpPanel } from '@/components/auth/sign-up-panel';
1769
+ import { clerkEnvReady } from '@/lib/demo-env';
1762
1770
 
1763
1771
  export default function SignUpPage() {
1772
+ if (!clerkEnvReady) {
1773
+ redirect('/');
1774
+ }
1775
+
1764
1776
  return (
1765
1777
  <main className="min-h-screen bg-background">
1766
1778
  <section className="container mx-auto flex min-h-screen max-w-5xl items-center justify-center px-4 py-8 sm:px-6">
@@ -1906,12 +1918,14 @@ export default function SignUpPage() {
1906
1918
  import { fileURLToPath } from 'node:url';
1907
1919
 
1908
1920
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
1921
+ const monorepoRoot = path.resolve(__dirname, '../..');
1909
1922
 
1910
1923
  /** @type {import('next').NextConfig} */
1911
1924
  const nextConfig = {
1912
1925
  reactCompiler: true,
1926
+ transpilePackages: ['@anhedral/db'],
1913
1927
  turbopack: {
1914
- root: __dirname,
1928
+ root: monorepoRoot,
1915
1929
  },
1916
1930
  };
1917
1931