diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dedf264fa..9de42f047 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,6 +62,7 @@ jobs: echo "SVIX_TOKEN=testsk_test" >> apps/app/.env.local echo "LIVEBLOCKS_SECRET=sk_test" >> apps/app/.env.local echo "BASEHUB_TOKEN=${{ secrets.BASEHUB_TOKEN }}" >> apps/app/.env.local + echo "VERCEL_PROJECT_PRODUCTION_URL=http://localhost:3002" >> apps/app/.env.local echo "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_JA==" >> apps/app/.env.local echo "NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in" >> apps/app/.env.local @@ -74,7 +75,6 @@ jobs: echo "NEXT_PUBLIC_APP_URL=http://localhost:3000" >> apps/app/.env.local echo "NEXT_PUBLIC_WEB_URL=http://localhost:3001" >> apps/app/.env.local echo "NEXT_PUBLIC_DOCS_URL=http://localhost:3004" >> apps/app/.env.local - echo "NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL=http://localhost:3002" >> apps/app/.env.local - name: Copy .env.local file run: | diff --git a/apps/api/.env.example b/apps/api/.env.example index ff406e542..fd29f1e65 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -13,6 +13,7 @@ ARCJET_KEY="" SVIX_TOKEN="" LIVEBLOCKS_SECRET="" BASEHUB_TOKEN="" +VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3002" # Client NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="" @@ -25,5 +26,4 @@ NEXT_PUBLIC_POSTHOG_KEY="" NEXT_PUBLIC_POSTHOG_HOST="" NEXT_PUBLIC_DOCS_URL="http://localhost:3004" NEXT_PUBLIC_APP_URL="http://localhost:3000" -NEXT_PUBLIC_WEB_URL="http://localhost:3001" -NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3002" \ No newline at end of file +NEXT_PUBLIC_WEB_URL="http://localhost:3001" \ No newline at end of file diff --git a/apps/api/app/webhooks/clerk/route.ts b/apps/api/app/webhooks/clerk/route.ts index 6d5483f2d..1b2919766 100644 --- a/apps/api/app/webhooks/clerk/route.ts +++ b/apps/api/app/webhooks/clerk/route.ts @@ -1,3 +1,4 @@ +import { env } from '@/env'; import { analytics } from '@repo/analytics/posthog/server'; import type { DeletedObjectJSON, @@ -6,7 +7,6 @@ import type { UserJSON, WebhookEvent, } from '@repo/auth/server'; -import { env } from '@repo/env'; import { log } from '@repo/observability/log'; import { headers } from 'next/headers'; import { NextResponse } from 'next/server'; diff --git a/apps/api/app/webhooks/stripe/route.ts b/apps/api/app/webhooks/stripe/route.ts index b20768c55..ef3e73db3 100644 --- a/apps/api/app/webhooks/stripe/route.ts +++ b/apps/api/app/webhooks/stripe/route.ts @@ -1,6 +1,6 @@ +import { env } from '@/env'; import { analytics } from '@repo/analytics/posthog/server'; import { clerkClient } from '@repo/auth/server'; -import { env } from '@repo/env'; import { parseError } from '@repo/observability/error'; import { log } from '@repo/observability/log'; import { stripe } from '@repo/payments'; diff --git a/apps/api/env.ts b/apps/api/env.ts new file mode 100644 index 000000000..a0596192a --- /dev/null +++ b/apps/api/env.ts @@ -0,0 +1,23 @@ +import { keys as analytics } from '@repo/analytics/keys'; +import { keys as auth } from '@repo/auth/keys'; +import { keys as database } from '@repo/database/keys'; +import { keys as email } from '@repo/email/keys'; +import { keys as core } from '@repo/next-config/keys'; +import { keys as observability } from '@repo/observability/keys'; +import { keys as payments } from '@repo/payments/keys'; +import { createEnv } from '@t3-oss/env-nextjs'; + +export const env = createEnv({ + extends: [ + auth(), + analytics(), + core(), + database(), + email(), + observability(), + payments(), + ], + server: {}, + client: {}, + runtimeEnv: {}, +}); diff --git a/apps/api/instrumentation.ts b/apps/api/instrumentation.ts index b8fbad46c..7bdbe0dc6 100644 --- a/apps/api/instrumentation.ts +++ b/apps/api/instrumentation.ts @@ -1,3 +1,3 @@ -import { initializeSentry } from '@repo/next-config/instrumentation'; +import { initializeSentry } from '@repo/observability/instrumentation'; export const register = initializeSentry(); diff --git a/apps/api/next.config.ts b/apps/api/next.config.ts index 36b4b15f9..aec710ef3 100644 --- a/apps/api/next.config.ts +++ b/apps/api/next.config.ts @@ -1,8 +1,9 @@ -import { env } from '@repo/env'; -import { config, withAnalyzer, withSentry } from '@repo/next-config'; +import { env } from '@/env'; +import { config, withAnalyzer } from '@repo/next-config'; +import { withLogtail, withSentry } from '@repo/observability/next-config'; import type { NextConfig } from 'next'; -let nextConfig: NextConfig = { ...config }; +let nextConfig: NextConfig = withLogtail({ ...config }); if (env.VERCEL) { nextConfig = withSentry(nextConfig); diff --git a/apps/api/package.json b/apps/api/package.json index 68bc409c5..cefdbd632 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -16,15 +16,16 @@ "@repo/auth": "workspace:*", "@repo/database": "workspace:*", "@repo/design-system": "workspace:*", - "@repo/env": "workspace:*", + "@repo/next-config": "workspace:*", "@repo/observability": "workspace:*", "@repo/payments": "workspace:*", "@sentry/nextjs": "^8.43.0", - "@repo/next-config": "workspace:*", + "@t3-oss/env-nextjs": "^0.11.1", "next": "15.1.0", "react": "19.0.0", "react-dom": "19.0.0", - "svix": "^1.43.0" + "svix": "^1.43.0", + "zod": "^3.23.8" }, "devDependencies": { "@repo/typescript-config": "workspace:*", diff --git a/apps/api/sentry.client.config.ts b/apps/api/sentry.client.config.ts index 9f17f9545..9a7f613ae 100644 --- a/apps/api/sentry.client.config.ts +++ b/apps/api/sentry.client.config.ts @@ -1,34 +1,3 @@ -/* - * This file configures the initialization of Sentry on the client. - * The config you add here will be used whenever a users loads a page in their browser. - * https://docs.sentry.io/platforms/javascript/guides/nextjs/ - */ +import { initializeSentry } from '@repo/observability/client'; -import { init, replayIntegration } from '@sentry/nextjs'; - -init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: 1, - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, - - replaysOnErrorSampleRate: 1, - - /* - * This sets the sample rate to be 10%. You may want this to be 100% while - * in development and sample at a lower rate in production - */ - replaysSessionSampleRate: 0.1, - - // You can remove this option if you're not planning to use the Sentry Session Replay feature: - integrations: [ - replayIntegration({ - // Additional Replay configuration goes in here, for example: - maskAllText: true, - blockAllMedia: true, - }), - ], -}); +initializeSentry(); diff --git a/apps/app/.env.example b/apps/app/.env.example index e10a0736d..c62797519 100644 --- a/apps/app/.env.example +++ b/apps/app/.env.example @@ -13,6 +13,7 @@ ARCJET_KEY="" SVIX_TOKEN="" LIVEBLOCKS_SECRET="" BASEHUB_TOKEN="" +VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3000" # Client NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="" @@ -25,5 +26,4 @@ NEXT_PUBLIC_POSTHOG_KEY="" NEXT_PUBLIC_POSTHOG_HOST="" NEXT_PUBLIC_APP_URL="http://localhost:3000" NEXT_PUBLIC_WEB_URL="http://localhost:3001" -NEXT_PUBLIC_DOCS_URL="http://localhost:3004" -NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3000" \ No newline at end of file +NEXT_PUBLIC_DOCS_URL="http://localhost:3004" \ No newline at end of file diff --git a/apps/app/app/(authenticated)/layout.tsx b/apps/app/app/(authenticated)/layout.tsx index 85650da5d..ea8bc8021 100644 --- a/apps/app/app/(authenticated)/layout.tsx +++ b/apps/app/app/(authenticated)/layout.tsx @@ -1,6 +1,6 @@ +import { env } from '@/env'; import { auth, currentUser } from '@repo/auth/server'; import { SidebarProvider } from '@repo/design-system/components/ui/sidebar'; -import { env } from '@repo/env'; import { showBetaFeature } from '@repo/feature-flags'; import { secure } from '@repo/security'; import type { ReactNode } from 'react'; diff --git a/apps/app/app/(authenticated)/page.tsx b/apps/app/app/(authenticated)/page.tsx index ac6189817..a1aebe80c 100644 --- a/apps/app/app/(authenticated)/page.tsx +++ b/apps/app/app/(authenticated)/page.tsx @@ -1,6 +1,6 @@ +import { env } from '@/env'; import { auth } from '@repo/auth/server'; import { database } from '@repo/database'; -import { env } from '@repo/env'; import type { Metadata } from 'next'; import dynamic from 'next/dynamic'; import { notFound } from 'next/navigation'; diff --git a/apps/app/app/(unauthenticated)/layout.tsx b/apps/app/app/(unauthenticated)/layout.tsx index 9bd4d3bec..35ac49877 100644 --- a/apps/app/app/(unauthenticated)/layout.tsx +++ b/apps/app/app/(unauthenticated)/layout.tsx @@ -1,5 +1,5 @@ +import { env } from '@/env'; import { ModeToggle } from '@repo/design-system/components/mode-toggle'; -import { env } from '@repo/env'; import { CommandIcon } from 'lucide-react'; import Link from 'next/link'; import type { ReactNode } from 'react'; diff --git a/apps/app/app/layout.tsx b/apps/app/app/layout.tsx index 6d89db474..a6f154440 100644 --- a/apps/app/app/layout.tsx +++ b/apps/app/app/layout.tsx @@ -1,6 +1,7 @@ import '@repo/design-system/styles/globals.css'; import { DesignSystemProvider } from '@repo/design-system'; import { fonts } from '@repo/design-system/lib/fonts'; +import { Toolbar } from '@repo/feature-flags/components/toolbar'; import type { ReactNode } from 'react'; type RootLayoutProperties = { @@ -11,6 +12,7 @@ const RootLayout = ({ children }: RootLayoutProperties) => ( {children} + ); diff --git a/apps/app/env.ts b/apps/app/env.ts new file mode 100644 index 000000000..1d9671108 --- /dev/null +++ b/apps/app/env.ts @@ -0,0 +1,29 @@ +import { keys as analytics } from '@repo/analytics/keys'; +import { keys as auth } from '@repo/auth/keys'; +import { keys as collaboration } from '@repo/collaboration/keys'; +import { keys as database } from '@repo/database/keys'; +import { keys as email } from '@repo/email/keys'; +import { keys as flags } from '@repo/feature-flags/keys'; +import { keys as core } from '@repo/next-config/keys'; +import { keys as observability } from '@repo/observability/keys'; +import { keys as security } from '@repo/security/keys'; +import { keys as webhooks } from '@repo/webhooks/keys'; +import { createEnv } from '@t3-oss/env-nextjs'; + +export const env = createEnv({ + extends: [ + auth(), + analytics(), + collaboration(), + core(), + database(), + email(), + flags(), + observability(), + security(), + webhooks(), + ], + server: {}, + client: {}, + runtimeEnv: {}, +}); diff --git a/apps/app/instrumentation.ts b/apps/app/instrumentation.ts index b8fbad46c..7bdbe0dc6 100644 --- a/apps/app/instrumentation.ts +++ b/apps/app/instrumentation.ts @@ -1,3 +1,3 @@ -import { initializeSentry } from '@repo/next-config/instrumentation'; +import { initializeSentry } from '@repo/observability/instrumentation'; export const register = initializeSentry(); diff --git a/apps/app/middleware.ts b/apps/app/middleware.ts index fab2b91ee..ac55d9c1a 100644 --- a/apps/app/middleware.ts +++ b/apps/app/middleware.ts @@ -1,7 +1,14 @@ import { authMiddleware } from '@repo/auth/middleware'; -import { noseconeConfig, noseconeMiddleware } from '@repo/security/middleware'; +import { + noseconeMiddleware, + noseconeOptions, + noseconeOptionsWithToolbar, +} from '@repo/security/middleware'; +import { env } from './env'; -const securityHeaders = noseconeMiddleware(noseconeConfig); +const securityHeaders = env.FLAGS_SECRET + ? noseconeMiddleware(noseconeOptionsWithToolbar) + : noseconeMiddleware(noseconeOptions); export default authMiddleware(() => securityHeaders()); diff --git a/apps/app/next.config.ts b/apps/app/next.config.ts index 36b4b15f9..bad3085e4 100644 --- a/apps/app/next.config.ts +++ b/apps/app/next.config.ts @@ -1,8 +1,10 @@ -import { env } from '@repo/env'; -import { config, withAnalyzer, withSentry } from '@repo/next-config'; +import { env } from '@/env'; +import { withToolbar } from '@repo/feature-flags/lib/toolbar'; +import { config, withAnalyzer } from '@repo/next-config'; +import { withLogtail, withSentry } from '@repo/observability/next-config'; import type { NextConfig } from 'next'; -let nextConfig: NextConfig = { ...config }; +let nextConfig: NextConfig = withToolbar(withLogtail({ ...config })); if (env.VERCEL) { nextConfig = withSentry(nextConfig); diff --git a/apps/app/package.json b/apps/app/package.json index 992e55b3a..158d69d66 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -17,14 +17,15 @@ "@repo/collaboration": "workspace:*", "@repo/database": "workspace:*", "@repo/design-system": "workspace:*", - "@repo/env": "workspace:*", "@repo/feature-flags": "workspace:*", "@repo/next-config": "workspace:*", + "@repo/observability": "workspace:*", "@repo/security": "workspace:*", "@repo/seo": "workspace:*", "@repo/tailwind-config": "workspace:*", "@repo/webhooks": "workspace:*", "@sentry/nextjs": "^8.43.0", + "@t3-oss/env-nextjs": "^0.11.1", "fuse.js": "^7.0.0", "import-in-the-middle": "^1.11.3", "lucide-react": "^0.468.0", @@ -32,7 +33,8 @@ "next-themes": "^0.4.4", "react": "19.0.0", "react-dom": "19.0.0", - "require-in-the-middle": "^7.4.0" + "require-in-the-middle": "^7.4.0", + "zod": "^3.23.8" }, "devDependencies": { "@repo/testing": "workspace:*", diff --git a/apps/app/sentry.client.config.ts b/apps/app/sentry.client.config.ts index 9f17f9545..9a7f613ae 100644 --- a/apps/app/sentry.client.config.ts +++ b/apps/app/sentry.client.config.ts @@ -1,34 +1,3 @@ -/* - * This file configures the initialization of Sentry on the client. - * The config you add here will be used whenever a users loads a page in their browser. - * https://docs.sentry.io/platforms/javascript/guides/nextjs/ - */ +import { initializeSentry } from '@repo/observability/client'; -import { init, replayIntegration } from '@sentry/nextjs'; - -init({ - dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, - - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: 1, - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, - - replaysOnErrorSampleRate: 1, - - /* - * This sets the sample rate to be 10%. You may want this to be 100% while - * in development and sample at a lower rate in production - */ - replaysSessionSampleRate: 0.1, - - // You can remove this option if you're not planning to use the Sentry Session Replay feature: - integrations: [ - replayIntegration({ - // Additional Replay configuration goes in here, for example: - maskAllText: true, - blockAllMedia: true, - }), - ], -}); +initializeSentry(); diff --git a/apps/web/.env.example b/apps/web/.env.example index a0c5738ed..d535f5e41 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -13,6 +13,7 @@ ARCJET_KEY="" SVIX_TOKEN="" LIVEBLOCKS_SECRET="" BASEHUB_TOKEN="" +VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3001" # Client NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="" @@ -26,5 +27,4 @@ NEXT_PUBLIC_POSTHOG_HOST="" NEXT_PUBLIC_APP_URL="http://localhost:3000" NEXT_PUBLIC_WEB_URL="http://localhost:3001" NEXT_PUBLIC_API_URL="http://localhost:3002" -NEXT_PUBLIC_DOCS_URL="http://localhost:3004" -NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL="http://localhost:3001" \ No newline at end of file +NEXT_PUBLIC_DOCS_URL="http://localhost:3004" \ No newline at end of file diff --git a/apps/web/app/(home)/components/cta.tsx b/apps/web/app/(home)/components/cta.tsx index 9370d6e3e..2b83d0840 100644 --- a/apps/web/app/(home)/components/cta.tsx +++ b/apps/web/app/(home)/components/cta.tsx @@ -1,5 +1,5 @@ +import { env } from '@/env'; import { Button } from '@repo/design-system/components/ui/button'; -import { env } from '@repo/env'; import { MoveRight, PhoneCall } from 'lucide-react'; import Link from 'next/link'; diff --git a/apps/web/app/(home)/components/hero.tsx b/apps/web/app/(home)/components/hero.tsx index a8a9a6675..90587ccba 100644 --- a/apps/web/app/(home)/components/hero.tsx +++ b/apps/web/app/(home)/components/hero.tsx @@ -1,7 +1,7 @@ +import { env } from '@/env'; import { blog } from '@repo/cms'; import { Feed } from '@repo/cms/components/feed'; import { Button } from '@repo/design-system/components/ui/button'; -import { env } from '@repo/env'; import { MoveRight, PhoneCall } from 'lucide-react'; import { draftMode } from 'next/headers'; import Link from 'next/link'; diff --git a/apps/web/app/blog/[slug]/page.tsx b/apps/web/app/blog/[slug]/page.tsx index cc65993c6..16f202ecb 100644 --- a/apps/web/app/blog/[slug]/page.tsx +++ b/apps/web/app/blog/[slug]/page.tsx @@ -1,11 +1,11 @@ import { Sidebar } from '@/components/sidebar'; +import { env } from '@/env'; import { ArrowLeftIcon } from '@radix-ui/react-icons'; import { blog } from '@repo/cms'; import { Body } from '@repo/cms/components/body'; import { Feed } from '@repo/cms/components/feed'; import { Image } from '@repo/cms/components/image'; import { TableOfContents } from '@repo/cms/components/toc'; -import { env } from '@repo/env'; import { JsonLd } from '@repo/seo/json-ld'; import { createMetadata } from '@repo/seo/metadata'; import type { Metadata } from 'next'; @@ -71,7 +71,7 @@ const BlogPost = async ({ params }: BlogPostProperties) => { '@type': 'WebPage', '@id': new URL( `/blog/${slug}`, - env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL + env.VERCEL_PROJECT_PRODUCTION_URL ).toString(), }, headline: page._title, diff --git a/apps/web/app/components/footer.tsx b/apps/web/app/components/footer.tsx index 4e6e2f8c8..d6e181203 100644 --- a/apps/web/app/components/footer.tsx +++ b/apps/web/app/components/footer.tsx @@ -1,4 +1,4 @@ -import { env } from '@repo/env'; +import { env } from '@/env'; import { Status } from '@repo/observability/status'; import Link from 'next/link'; diff --git a/apps/web/app/components/header/index.tsx b/apps/web/app/components/header/index.tsx index 53b2fb760..fb7d1ec62 100644 --- a/apps/web/app/components/header/index.tsx +++ b/apps/web/app/components/header/index.tsx @@ -1,5 +1,6 @@ 'use client'; +import { env } from '@/env'; import { ModeToggle } from '@repo/design-system/components/mode-toggle'; import { Button } from '@repo/design-system/components/ui/button'; import { @@ -10,7 +11,6 @@ import { NavigationMenuList, NavigationMenuTrigger, } from '@repo/design-system/components/ui/navigation-menu'; -import { env } from '@repo/env'; import { Menu, MoveRight, X } from 'lucide-react'; import Link from 'next/link'; import { useState } from 'react'; diff --git a/apps/web/app/contact/actions/contact.tsx b/apps/web/app/contact/actions/contact.tsx index 72f28a7cd..6db2fc109 100644 --- a/apps/web/app/contact/actions/contact.tsx +++ b/apps/web/app/contact/actions/contact.tsx @@ -1,8 +1,8 @@ 'use server'; +import { env } from '@/env'; import { resend } from '@repo/email'; import { ContactTemplate } from '@repo/email/templates/contact'; -import { env } from '@repo/env'; import { parseError } from '@repo/observability/error'; import { createRateLimiter, slidingWindow } from '@repo/rate-limit'; import { headers } from 'next/headers'; diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 58657bddc..d8034618f 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -3,6 +3,7 @@ import './styles/web.css'; import { DesignSystemProvider } from '@repo/design-system'; import { fonts } from '@repo/design-system/lib/fonts'; import { cn } from '@repo/design-system/lib/utils'; +import { Toolbar } from '@repo/feature-flags/components/toolbar'; import type { ReactNode } from 'react'; import { Footer } from './components/footer'; import { Header } from './components/header'; @@ -23,6 +24,7 @@ const RootLayout = ({ children }: RootLayoutProperties) => ( {children}