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}
+