Compare commits

...
3 Commits
Author SHA1 Message Date
Gabriel Brown 7d1884be41 Deploy from CI with Infisical and ship Payload changes as migrations
Build and Deploy Next App / quality (push) Successful in 1m24s
Build and Deploy Next App / convex-deploy (push) Successful in 1m58s
Build and Deploy Next App / build-next (push) Successful in 5m32s
CI reads Infisical staging as the read-only convexmonorepo-ci machine identity, deploys the Convex functions, builds and pushes the image, and asks the VPS to redeploy. Payload schema changes now ship as migrations that production runs on startup; the baseline matches the production schema and is recorded as applied there. The local stack moves to its own ports and syncs the Convex runtime env, and the scripts pick up Infisical account switching, dev-kill and a cache-safe CI install.
2026-10-01 14:42:55 -04:00
Gabriel Brown f39376a887 Add PostHog, move Plausible to v4, and drop Sentry session replay
PostHog loads after the page is idle through a same-origin /ingest proxy and follows the signed-in Convex user. Plausible uses the per-site script through a /pa proxy, clear of Payload's /api routes. Sentry keeps errors and traces only. Pages load only the @gib/ui modules they import, and client code reads public env from publicEnv.ts so zod stays out of the browser.
2026-10-01 14:42:29 -04:00
Gabriel Brown 1af6b43b1a Update dependencies and fix what the new lint rules caught
Every package moves to its newest in-major version, plus React 19.3, @convex-dev/auth 0.0.96 and next-plausible 4. The backend declares @auth/core and generates sign-in codes without oslo, which @convex-dev/auth no longer pulls in. The UseSend patch now actually removes the crypto import Convex refuses to bundle. The stricter react-hooks and typescript-eslint rules flagged the carousel, chart, image crop and mobile hook, which now pass.
2026-10-01 14:41:58 -04:00
56 changed files with 7352 additions and 1485 deletions

No files matched your search

+94 -19
View File
@@ -1,56 +1,131 @@
name: Build and Push Next App
name: Build and Deploy Next App
on:
push:
branches: [main]
branches:
- main
paths:
- 'apps/**'
- 'apps/next/**'
- 'packages/**'
- 'tools/**'
- 'scripts/**'
- 'docker/**'
- '.gitea/workflows/build-next.yml'
- '.infisical.json'
- 'package.json'
- 'bun.lock'
- 'turbo.json'
# One production deploy at a time; a newer push waits rather than racing.
concurrency:
group: build-next-production
cancel-in-progress: false
# Production env comes from the Infisical `staging` environment. Each job logs
# in as the read-only `convexmonorepo-ci` machine identity and writes the env to a
# temp file (scripts/ci-export-env.mjs), so no prod values live in Gitea.
jobs:
# Quality gate: workspace lint (sherif), eslint, typecheck and the unit,
# integration and component suites must pass before anything touches
# production. The local-stack e2e smoke check is skipped in CI.
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
- run: bun install --frozen-lockfile
- name: Lint, typecheck, and test
bun-version: 1.3.14
- name: Install dependencies
run: ./scripts/ci-install
- name: Lint and typecheck
env:
DOTENV_PROD: ${{ secrets.DOTENV_PROD }}
INFISICAL_CLIENT_ID: ${{ secrets.INFISICAL_CLIENT_ID }}
INFISICAL_CLIENT_SECRET: ${{ secrets.INFISICAL_CLIENT_SECRET }}
run: |
env_file="$(mktemp)"
trap 'rm -f "$env_file"' EXIT
printf '%s\n' "$DOTENV_PROD" > "$env_file"
node scripts/ci-export-env.mjs staging > "$env_file"
bunx dotenv -e "$env_file" -- env NODE_ENV=test SKIP_E2E=1 bun run ci:check
build-next:
# Pushes Convex functions and schema to prod before the image that depends on
# them is built. The env file must carry CONVEX_SELF_HOSTED_URL and
# CONVEX_SELF_HOSTED_ADMIN_KEY. Codegen is disabled: _generated is committed.
convex-deploy:
needs: [quality]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to container registry
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.gbrown.org -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Build image
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
- name: Install dependencies
run: ./scripts/ci-install
- name: Deploy Convex functions
env:
DOTENV_PROD: ${{ secrets.DOTENV_PROD }}
INFISICAL_CLIENT_ID: ${{ secrets.INFISICAL_CLIENT_ID }}
INFISICAL_CLIENT_SECRET: ${{ secrets.INFISICAL_CLIENT_SECRET }}
run: |
env_file="$(mktemp)"
trap 'rm -f "$env_file"' EXIT
printf '%s\n' "$DOTENV_PROD" > "$env_file"
node scripts/ci-export-env.mjs staging > "$env_file"
cd packages/backend
bunx convex deploy --env-file "$env_file" --codegen disable --typecheck disable
build-next:
needs: [convex-deploy]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to container registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.gbrown.org -u "${{ secrets.REGISTRY_USER }}" --password-stdin
- name: Build app using project script
env:
INFISICAL_CLIENT_ID: ${{ secrets.INFISICAL_CLIENT_ID }}
INFISICAL_CLIENT_SECRET: ${{ secrets.INFISICAL_CLIENT_SECRET }}
run: |
env_file="$(mktemp)"
trap 'rm -f "$env_file"' EXIT
node scripts/ci-export-env.mjs staging > "$env_file"
CI_ENV_FILE="$env_file" ./scripts/build-next-app staging
- name: Tag and push image
- name: Tag built image
run: |
docker tag convexmonorepo-next:latest git.gbrown.org/gib/convexmonorepo-next:${{ gitea.sha }}
docker tag convexmonorepo-next:latest git.gbrown.org/gib/convexmonorepo-next:latest
- name: Push image tags
run: |
docker push git.gbrown.org/gib/convexmonorepo-next:${{ gitea.sha }}
docker push git.gbrown.org/gib/convexmonorepo-next:latest
# Asks the VPS to pull the new image and restart podman-convexmonorepo.service
# through the forced-command deploy key (~/Server/AGENTS.md §15). A
# failure only warns: the image is pushed and the midnight
# podman-update.timer deploys it anyway.
- name: Trigger immediate deploy
run: |
install -m600 /dev/null "$HOME/.deploy_key"
printf '%s\n' "${{ secrets.DEPLOY_SSH_KEY }}" > "$HOME/.deploy_key"
ssh -i "$HOME/.deploy_key" \
-o BatchMode=yes \
-o StrictHostKeyChecking=accept-new \
-o ConnectTimeout=15 \
[email protected] convexmonorepo \
|| echo "::warning::deploy trigger failed; nightly sweep will deploy instead"
rm -f "$HOME/.deploy_key"
+3
View File
@@ -66,3 +66,6 @@ apps/next/playwright-report/
# tanstack
.tanstack
# local command flags (machine-specific, e.g. which Infisical account to use)
scripts/flags/
+2 -1
View File
@@ -1,5 +1,6 @@
{
"workspaceId": "b2760150-c581-4e0d-acb5-b4da51cee58d",
"defaultEnvironment": "",
"gitBranchToEnvironmentMapping": null
"gitBranchToEnvironmentMapping": null,
"domain": "https://infisical.gbrown.org/api"
}
+48 -6
View File
@@ -7,8 +7,8 @@
- `packages/backend/convex`: self-hosted Convex functions, schema, and auth.
- `packages/ui`: shared shadcn-based UI components.
- `tools`: shared ESLint, Prettier, Tailwind, TypeScript, and Vitest config.
- Local development uses host-run apps, local Convex on ports 3210/3211, its
own data volume, and Payload Postgres on port 5432. Convex does not use
- Local development uses host-run apps, local Convex on ports 3240/3241, its
own data volume, and Payload Postgres on port 5436. Convex does not use
Postgres unless a cloned project explicitly enables the commented option.
## Protected and generated files
@@ -29,8 +29,17 @@
- Run `infisical login` and `infisical init` before local development.
- Machine-generated values belong in `.local/<env>.generated.env`; never put
the generated Convex admin key in shared Infisical.
- CI uses Gitea-injected secrets or `CI_ENV_FILE` and must not call Infisical.
- App code imports validated variables from `@/env`, never `process.env`.
- CI reads `staging` through `scripts/ci-export-env.mjs`, logged in as the
read-only `convexmonorepo-ci` machine identity (Gitea secrets
`INFISICAL_CLIENT_ID` and `INFISICAL_CLIENT_SECRET`). `scripts/export-env`
refuses to run in CI.
- `staging` holds the production values that CI builds and deploys with.
- If you are logged in to more than one Infisical account, put
`INFISICAL_EMAIL=...` in `scripts/flags/infisical.env` (gitignored) and the
scripts switch the CLI to that account before exporting.
- Server code imports validated variables from `@/env`. Client components and
`instrumentation-client.ts` import from `@/publicEnv`, which keeps zod out of
the browser bundle. Never read `process.env` elsewhere.
- Add cache-relevant variables to `turbo.json` `globalEnv`.
## Code and dependency rules
@@ -52,10 +61,12 @@ bun dev:next # host Next + deploy/watch local Convex functions
bun db:down # stop and preserve both data volumes
bun db:down:wipe # remove both volumes, generated admin key, and seed marker
bun db:sync:payload # refresh and apply the local Payload seed snapshot
bun db:sync-env # re-push Infisical dev values into the local Convex env
bun dev:kill # stop dev servers that survived a Ctrl-C
```
Local URLs: app `http://localhost:3000`, Convex `http://localhost:3210`,
dashboard `http://localhost:6791`, Payload Postgres `localhost:5432`.
Local URLs: app `http://localhost:3000`, Convex `http://localhost:3240`,
dashboard `http://localhost:6794`, Payload Postgres `localhost:5436`.
Use `INFISICAL_ENV=staging bun dev:next` for staging services.
`db:sync:payload` is an explicit, destructive local operation. It validates a
@@ -65,6 +76,37 @@ only under `.local/`, and never changes Convex or production data. Normal
when `.local/payload-seed-state.env` is absent or stale. Do not store seed
markers in Payload Postgres; schema push treats unmanaged tables as drift.
## Payload schema changes
Payload schema changes ship as migrations in `apps/next/src/migrations/`.
Locally, Payload pushes schema edits straight into the localhost database. When
a change is ready, run `bun with-env bunx payload migrate:create <name>` from
`apps/next` and commit the generated files. Production runs pending migrations
on startup (`prodMigrations` in `payload.config.ts`). Push only runs against a
database on `localhost`: a push leaves a `dev` row in `payload_migrations`, and
Payload's startup migration stops on a confirmation prompt when it finds one.
For the same reason, a local production build against the local dev database
hangs; point it at a database without that row.
## Analytics and bundle size
- Plausible (`NEXT_PUBLIC_PLAUSIBLE_SRC`, the per-site script URL) loads through
a same-origin proxy under `/pa`, clear of Payload's `/api` routes. PostHog
(`NEXT_PUBLIC_POSTHOG_KEY`, `NEXT_PUBLIC_POSTHOG_HOST`) loads after the page
is idle through `/ingest`. Both stay off when their env is unset.
- Session replay comes from PostHog. Sentry carries errors and traces only.
- `packages/ui/src/index.tsx` holds re-exports only (`cn` and `ccn` live in
`utils.ts`). With `optimizePackageImports` in `next.config.js`, each page
loads only the UI modules it imports. Declaring anything in the index breaks
that.
## Deployment
A push to `main` runs `.gitea/workflows/build-next.yml`: the quality gate, then
`convex deploy` to production, then the image build and push, then
`ssh [email protected] convexmonorepo` (the forced-command deploy on the VPS). A
push to `main` is a production deploy.
## Validation
Use `bun typecheck`, never a production build for routine validation. The full
+18 -8
View File
@@ -5,7 +5,8 @@ Payload CMS, shared UI/config packages, Vitest, and Docker deployment.
## Local setup
Requirements: Bun 1.3.10, Docker or Podman, Node 22, and the Infisical CLI.
Requirements: Bun 1.3.14, Docker or Podman, Node 22, the Infisical CLI, `jq`, and
`flock`.
```sh
bun install --frozen-lockfile
@@ -22,9 +23,9 @@ files. Select staging with `INFISICAL_ENV=staging bun dev:next`.
Local services:
- Next.js: `http://localhost:3000`
- Convex API: `http://localhost:3210`
- Convex dashboard: `http://localhost:6791`
- Payload Postgres: `localhost:5432`
- Convex API: `http://localhost:3240`
- Convex dashboard: `http://localhost:6794`
- Payload Postgres: `localhost:5436`
Next and Expo run on the host. Payload uses the local Postgres database. Convex
uses a separate self-hosted data volume and does not use Postgres by default.
@@ -54,7 +55,8 @@ Convex URL with the development host's LAN address when testing Expo on-device.
- Local `dev` and `staging`: Infisical.
- Generated local state: `.local/<environment>.generated.env`.
- CI/CD: Gitea `DOTENV_PROD`, materialized only as a temporary runner file.
- CI/CD: Infisical `staging`, read by the read-only `convexmonorepo-ci` machine
identity and materialized only as a temporary runner file.
- Docker compilation: explicit Compose build args; `.env*` stays outside the
image context.
@@ -87,6 +89,14 @@ Shared dependency versions belong in root catalogs. Edit the root catalog, run
Production Compose retains the self-hosted Convex backend/dashboard and accepts
an external Payload `PAYLOAD_DB_URL`. Its commented Postgres service remains an
optional Payload database. Gitea runs the quality gate first, builds the Next
image from a temporary Gitea-secret env file, then pushes SHA and `latest` tags.
CI never installs or invokes Infisical.
optional Payload database.
A push to `main` runs the quality gate, deploys the Convex functions, builds the
Next image, pushes SHA and `latest` tags, and asks the VPS to redeploy
`podman-convexmonorepo.service` over the forced-command deploy key. If that last
step fails, the nightly update timer deploys the image instead. Each job reads
Infisical `staging` through `scripts/ci-export-env.mjs` as the `convexmonorepo-ci`
machine identity (Gitea secrets `INFISICAL_CLIENT_ID` and
`INFISICAL_CLIENT_SECRET`).
Payload schema changes ship as migrations; see `AGENTS.md`.
+11 -11
View File
@@ -20,26 +20,26 @@
"@expo/vector-icons": "^15.1.1",
"@gib/backend": "workspace:*",
"@legendapp/list": "^2.0.19",
"@react-navigation/bottom-tabs": "^7.15.8",
"@react-navigation/elements": "^2.9.13",
"@react-navigation/native": "^7.2.1",
"@react-navigation/bottom-tabs": "^7.20.0",
"@react-navigation/elements": "^2.9.44",
"@react-navigation/native": "^7.5.0",
"@sentry/react-native": "^7.13.0",
"convex": "catalog:convex",
"expo": "~54.0.33",
"expo": "~54.0.37",
"expo-apple-authentication": "~8.0.8",
"expo-constants": "~18.0.13",
"expo-dev-client": "~6.0.20",
"expo-font": "~14.0.11",
"expo-constants": "~18.0.14",
"expo-dev-client": "~6.0.21",
"expo-font": "~14.0.12",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-linking": "~8.0.11",
"expo-router": "~6.0.23",
"expo-linking": "~8.0.12",
"expo-router": "~6.0.24",
"expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8",
"expo-system-ui": "~6.0.9",
"expo-web-browser": "~15.0.10",
"expo-web-browser": "~15.0.11",
"nativewind": "5.0.0-preview.2",
"react": "catalog:react19",
"react-dom": "catalog:react19",
@@ -49,7 +49,7 @@
"react-native-reanimated": "~4.1.7",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.16.0",
"react-native-web": "~0.21.2",
"react-native-web": "~0.21.3",
"react-native-worklets": "~0.5.2"
},
"devDependencies": {
+1
View File
@@ -3,6 +3,7 @@
"compilerOptions": {
"jsx": "react-native",
"checkJs": false,
"types": ["node"],
"moduleSuffixes": [".ios", ".android", ".native", ""],
"paths": {
"~/*": ["./src/*"]
+1 -1
View File
@@ -6,7 +6,7 @@ import { reactConfig } from '@gib/eslint-config/react';
export default defineConfig(
{
ignores: ['.next/**', 'payload-types.ts'],
ignores: ['.next/**', 'payload-types.ts', 'src/migrations/**'],
},
baseConfig,
reactConfig,
+52 -5
View File
@@ -4,12 +4,16 @@ import { createJiti } from 'jiti';
import { withPlausibleProxy } from 'next-plausible';
const jiti = createJiti(import.meta.url);
await jiti.import('./src/env');
const { env } = /** @type {typeof import('./src/env')} */ (
await jiti.import('./src/env')
);
const { posthogScriptAliases } =
/** @type {typeof import('./src/lib/posthog-scripts')} */ (
await jiti.import('./src/lib/posthog-scripts')
);
/** @type {import("next").NextConfig} */
const config = withPlausibleProxy({
customDomain: process.env.NEXT_PUBLIC_PLAUSIBLE_URL,
})({
const baseConfig = {
output: 'standalone',
images: {
remotePatterns: [
@@ -24,11 +28,54 @@ const config = withPlausibleProxy({
serverActions: {
bodySizeLimit: '10mb',
},
// Resolves `import { Button } from '@gib/ui'` to the button module alone,
// so marketing pages never pull in admin-only Radix widgets or Recharts.
// radix-ui is the umbrella package the shadcn components import from.
// lucide-react, date-fns and @tabler/icons-react are on Next's default list.
optimizePackageImports: ['@gib/ui', 'radix-ui'],
},
/** Enables hot reloading for local packages without a build step */
transpilePackages: ['@gib/backend', '@gib/ui'],
typescript: { ignoreBuildErrors: true },
});
// PostHog's capture paths end in a slash (/e/, /i/v0/e/), and Next's
// built-in trailing-slash redirect would answer those proxied POSTs with a
// redirect. It is turned off here and replaced by the redirect below, which
// strips trailing slashes everywhere except /ingest.
skipTrailingSlashRedirect: true,
redirects: async () => [
{
source: '/:path((?!ingest(?:/|$)).+)/',
destination: '/:path',
permanent: true,
},
],
// Same-origin proxy for PostHog, so blocklists that match the PostHog host
// never see the request. The aliased extension scripts (blocked by file
// name, see src/lib/posthog-scripts.ts) map back to their real files first.
rewrites: async () => {
const host = env.NEXT_PUBLIC_POSTHOG_HOST;
if (!host) return [];
return [
...Object.entries(posthogScriptAliases).map(([name, alias]) => ({
source: `/ingest/static/${alias}.js`,
destination: `${host}/static/${name}.js`,
})),
{ source: '/ingest/:path*', destination: `${host}/:path*` },
];
},
};
// Serves Plausible's per-site script from this origin so ad blockers see a
// first-party request. next-plausible 4 takes the full script URL. Its default
// event path is /api/event, which sits inside Payload's /api routes, so both
// paths move under /pa.
const config = env.NEXT_PUBLIC_PLAUSIBLE_SRC
? withPlausibleProxy({
src: env.NEXT_PUBLIC_PLAUSIBLE_SRC,
scriptPath: '/pa/script.js',
apiPath: '/pa/event',
})(baseConfig)
: baseConfig;
const sentryConfig = {
// For all available options, see:
+11 -10
View File
@@ -23,20 +23,21 @@
"@convex-dev/auth": "catalog:convex",
"@gib/backend": "workspace:*",
"@gib/ui": "workspace:*",
"@payloadcms/db-postgres": "^3.80.0",
"@payloadcms/live-preview-react": "^3.80.0",
"@payloadcms/next": "^3.80.0",
"@payloadcms/richtext-lexical": "^3.80.0",
"@sentry/nextjs": "^10.46.0",
"@payloadcms/db-postgres": "^3.90.2",
"@payloadcms/live-preview-react": "^3.90.2",
"@payloadcms/next": "^3.90.2",
"@payloadcms/richtext-lexical": "^3.90.2",
"@sentry/nextjs": "^10.75.3",
"@t3-oss/env-nextjs": "^0.13.11",
"convex": "catalog:convex",
"next": "^16.2.1",
"next-plausible": "^3.12.5",
"payload": "^3.80.0",
"next": "^16.3.8",
"next-plausible": "^4.0.0",
"payload": "^3.90.2",
"posthog-js": "^1.435.6",
"react": "catalog:react19",
"react-dom": "catalog:react19",
"require-in-the-middle": "^7.5.2",
"superjson": "2.2.3",
"superjson": "2.2.6",
"ws": "catalog:",
"zod": "catalog:"
},
@@ -50,7 +51,7 @@
"@types/node": "catalog:",
"@types/react": "catalog:react19",
"@types/react-dom": "catalog:react19",
"baseline-browser-mapping": "^2.10.11",
"baseline-browser-mapping": "^2.11.26",
"eslint": "catalog:",
"jsdom": "catalog:test",
"prettier": "catalog:",
+2
View File
@@ -137,6 +137,7 @@ export interface User {
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
resetPasswordRequestedAt?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
sessions?:
@@ -230,6 +231,7 @@ export interface UsersSelect<T extends boolean = true> {
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
resetPasswordRequestedAt?: T;
loginAttempts?: T;
lockUntil?: T;
sessions?:
+35 -33
View File
@@ -10,8 +10,8 @@ import { useEffect } from 'react';
import Footer from '@/components/layout/footer';
import Header from '@/components/layout/header';
import { ConvexClientProvider } from '@/components/providers';
import { env } from '@/env';
import { generateMetadata } from '@/lib/metadata';
import { publicEnv } from '@/publicEnv';
import * as Sentry from '@sentry/nextjs';
import PlausibleProvider from 'next-plausible';
@@ -44,39 +44,41 @@ const GlobalError = ({ error, reset = undefined }: GlobalErrorProps) => {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<PlausibleProvider
domain={env.NEXT_PUBLIC_SITE_URL.trim().replace(/^https?:\/\//, '')}
customDomain={env.NEXT_PUBLIC_PLAUSIBLE_URL}
const page = (
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
<html lang='en' suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
<ConvexClientProvider>
<main className='flex min-h-screen flex-col items-center'>
<Header />
<NextError statusCode={0} />
{reset !== undefined && (
<Button onClick={() => reset()}>Try Again</Button>
)}
<Toaster />
<Footer />
</main>
<main className='flex min-h-[90vh] flex-col items-center'>
<Toaster />
</main>
</ConvexClientProvider>
</ThemeProvider>
</body>
</html>
</PlausibleProvider>
<ConvexClientProvider>
<main className='flex min-h-screen flex-col items-center'>
<Header />
<NextError statusCode={0} />
{reset !== undefined && (
<Button onClick={() => reset()}>Try Again</Button>
)}
<Toaster />
<Footer />
</main>
<main className='flex min-h-[90vh] flex-col items-center'>
<Toaster />
</main>
</ConvexClientProvider>
</ThemeProvider>
);
return (
<html lang='en' suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{publicEnv.NEXT_PUBLIC_PLAUSIBLE_SRC ? (
<PlausibleProvider>{page}</PlausibleProvider>
) : (
page
)}
</body>
</html>
);
};
export default GlobalError;
+30 -26
View File
@@ -36,34 +36,38 @@ const RootLayout = ({
}: Readonly<{
children: React.ReactNode;
}>) => {
const app = (
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
<ConvexClientProvider>
<div className='flex min-h-screen flex-col'>
<Header />
{children}
<Footer />
</div>
<Toaster />
</ConvexClientProvider>
</ThemeProvider>
);
return (
<ConvexAuthNextjsServerProvider>
<PlausibleProvider
domain={env.NEXT_PUBLIC_SITE_URL.trim().replace(/^https?:\/\//, '')}
customDomain={env.NEXT_PUBLIC_PLAUSIBLE_URL}
>
<html lang='en' suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
<ConvexClientProvider>
<div className='flex min-h-screen flex-col'>
<Header />
{children}
<Footer />
</div>
<Toaster />
</ConvexClientProvider>
</ThemeProvider>
</body>
</html>
</PlausibleProvider>
<html lang='en' suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{/* Plausible loads its per-site script through the same-origin
proxy in next.config.js; without that config it stays off. */}
{env.NEXT_PUBLIC_PLAUSIBLE_SRC ? (
<PlausibleProvider>{app}</PlausibleProvider>
) : (
app
)}
</body>
</html>
</ConvexAuthNextjsServerProvider>
);
};
@@ -2,6 +2,7 @@
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { resetAnalyticsIdentity } from '@/lib/analytics';
import { useAuthActions } from '@convex-dev/auth/react';
import { LogOut } from 'lucide-react';
@@ -22,6 +23,7 @@ export const SignOutForm = () => {
setIsSigningOut(true);
try {
await signOut();
resetAnalyticsIdentity();
router.push('/');
} catch (error) {
console.error('Sign out error:', error);
@@ -2,6 +2,7 @@
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { resetAnalyticsIdentity, useAnalyticsIdentity } from '@/lib/analytics';
import { useAuthActions } from '@convex-dev/auth/react';
import { useConvexAuth, useQuery } from 'convex/react';
@@ -23,6 +24,7 @@ export const AvatarDropdown = () => {
const { isLoading, isAuthenticated } = useConvexAuth();
const { signOut } = useAuthActions();
const user = useQuery(api.auth.getUser, {});
useAnalyticsIdentity(user?._id);
const currentImageUrl = useQuery(
api.files.getImageUrl,
user?.image ? { storageId: user.image as Id<'_storage'> } : 'skip',
@@ -77,6 +79,7 @@ export const AvatarDropdown = () => {
<button
onClick={() =>
void signOut().then(() => {
resetAnalyticsIdentity();
router.push('/');
})
}
@@ -1,11 +1,11 @@
'use client';
import type { ReactNode } from 'react';
import { env } from '@/env';
import { publicEnv } from '@/publicEnv';
import { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs';
import { ConvexReactClient } from 'convex/react';
const convex = new ConvexReactClient(env.NEXT_PUBLIC_CONVEX_URL);
const convex = new ConvexReactClient(publicEnv.NEXT_PUBLIC_CONVEX_URL);
export const ConvexClientProvider = ({ children }: { children: ReactNode }) => (
<ConvexAuthNextjsProvider client={convex}>
+7 -2
View File
@@ -20,7 +20,10 @@ export const env = createEnv({
client: {
NEXT_PUBLIC_SITE_URL: z.url(),
NEXT_PUBLIC_CONVEX_URL: z.url(),
NEXT_PUBLIC_PLAUSIBLE_URL: z.url(),
// Analytics are optional: absent values leave the integration off.
NEXT_PUBLIC_PLAUSIBLE_SRC: z.url().optional(),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_HOST: z.url().optional(),
NEXT_PUBLIC_SENTRY_DSN: z.string(),
NEXT_PUBLIC_SENTRY_URL: z.string(),
NEXT_PUBLIC_SENTRY_ORG: z.string(),
@@ -38,7 +41,9 @@ export const env = createEnv({
PAYLOAD_DB_URL: process.env.PAYLOAD_DB_URL,
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL,
NEXT_PUBLIC_PLAUSIBLE_URL: process.env.NEXT_PUBLIC_PLAUSIBLE_URL,
NEXT_PUBLIC_PLAUSIBLE_SRC: process.env.NEXT_PUBLIC_PLAUSIBLE_SRC,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_SENTRY_URL: process.env.NEXT_PUBLIC_SENTRY_URL,
NEXT_PUBLIC_SENTRY_ORG: process.env.NEXT_PUBLIC_SENTRY_ORG,
+24 -16
View File
@@ -1,27 +1,35 @@
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import { env } from '@/env';
import { startAnalytics } from '@/lib/analytics';
import { publicEnv } from '@/publicEnv';
import * as Sentry from '@sentry/nextjs';
// Session replay is left to PostHog, so Sentry carries no replay integration.
Sentry.init({
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
integrations: [
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false,
}),
Sentry.feedbackIntegration({
colorScheme: 'system',
}),
],
dsn: publicEnv.NEXT_PUBLIC_SENTRY_DSN,
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii
sendDefaultPii: true,
sendDefaultPii: false,
// https://docs.sentry.io/platforms/javascript/configuration/options/#traces-sample-rate
tracesSampleRate: 1,
// A tenth of page loads is plenty for performance trends and keeps the
// self-hosted Sentry light.
tracesSampleRate: 0.1,
enableLogs: true,
// https://docs.sentry.io/platforms/javascript/session-replay/configuration/#general-integration-configuration
replaysSessionSampleRate: 0.5,
replaysOnErrorSampleRate: 1.0,
debug: false,
});
// The feedback widget is a floating button nobody needs during the first
// paint, so its code is split into its own chunk and added once the page has
// loaded.
const addFeedbackWidget = async () => {
const { feedbackIntegration } = await import('@sentry/nextjs');
Sentry.addIntegration(feedbackIntegration({ colorScheme: 'system' }));
};
if (document.readyState === 'complete') void addFeedbackWidget();
else
window.addEventListener('load', () => void addFeedbackWidget(), {
once: true,
});
startAnalytics();
// `captureRouterTransitionStart` is available from SDK version 9.12.0 onwards
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
+86
View File
@@ -0,0 +1,86 @@
import type { PostHog } from 'posthog-js';
import { useEffect } from 'react';
import { aliasPosthogScript } from '@/lib/posthog-scripts';
import { publicEnv } from '@/publicEnv';
// PostHog stays off until a project key and host are configured. Calling
// posthog-js before init logs an error on every call, so every use goes
// through this flag.
export const analyticsEnabled = Boolean(
publicEnv.NEXT_PUBLIC_POSTHOG_KEY && publicEnv.NEXT_PUBLIC_POSTHOG_HOST,
);
let client: PostHog | undefined;
// Identity calls made before PostHog has loaded, replayed in order once it has.
const pending: ((posthog: PostHog) => void)[] = [];
const withPosthog = (call: (posthog: PostHog) => void) => {
if (client) call(client);
else pending.push(call);
};
// Runs once the page has loaded and the main thread is idle, so analytics
// never competes with the first paint or hydration.
const afterPageIsIdle = (run: () => void) => {
const schedule = () => {
if ('requestIdleCallback' in window)
window.requestIdleCallback(run, { timeout: 4_000 });
else setTimeout(run, 0);
};
if (document.readyState === 'complete') schedule();
else window.addEventListener('load', schedule, { once: true });
};
const initPosthog = async (key: string) => {
const { default: posthog } = await import('posthog-js');
posthog.init(key, {
// Events go through the same-origin /ingest rewrite in next.config.js
// rather than straight to the PostHog host, whose name and paths ad
// blockers match on. ui_host keeps toolbar links on the real host.
api_host: '/ingest',
ui_host: publicEnv.NEXT_PUBLIC_POSTHOG_HOST,
// Makes pageviews follow App Router client-side navigation.
defaults: '2026-08-30',
// Autocapture, session replay (typed input masked), heatmaps and web
// vitals come from these defaults plus the project settings in PostHog.
capture_exceptions: true,
// Two extension script names are ad-blocked by name; see
// lib/posthog-scripts.ts for the aliasing.
strict_script_versioning: false,
prepare_external_dependency_script: (script) => {
script.src = aliasPosthogScript(script.src);
return script;
},
});
// Keeps local runs separate from production in the same project.
posthog.register({
environment: new URL(publicEnv.NEXT_PUBLIC_SITE_URL).hostname,
});
client = posthog;
for (const call of pending.splice(0)) call(posthog);
};
// Called once from instrumentation-client. posthog-js is downloaded and
// initialised only after the page is idle, which keeps it out of the first
// load; identity calls made in the meantime are queued above.
export const startAnalytics = () => {
const key = publicEnv.NEXT_PUBLIC_POSTHOG_KEY;
if (!analyticsEnabled || !key) return;
afterPageIsIdle(() => void initPosthog(key));
};
// Ties this browser's PostHog person to the signed-in Convex user. The id is
// opaque, so no name or email leaves the app. Mounted by the header avatar,
// which renders on every page while someone is signed in.
export const useAnalyticsIdentity = (userId: string | undefined) => {
useEffect(() => {
if (!analyticsEnabled || !userId) return;
withPosthog((posthog) => posthog.identify(userId));
}, [userId]);
};
// Starts a fresh anonymous id on sign-out so the next person on this browser
// is never merged into the last one.
export const resetAnalyticsIdentity = () => {
if (analyticsEnabled) withPosthog((posthog) => posthog.reset());
};
+24
View File
@@ -0,0 +1,24 @@
// posthog-js loads optional features (session replay, dead clicks) as extra
// scripts from <api_host>/static/<name>.js. Two of those file names sit on the
// default ad-blocker lists by name alone (EasyPrivacy carries the literal
// substrings /posthog-recorder.js and /dead-clicks-autocapture.js), so the
// same-origin proxy does not save them: the browser refuses the request before
// it leaves. The client asks for the alias instead, and next.config.js rewrites
// the alias back to the real file on the way out. Nothing here changes what is
// loaded, only what it is called on the wire. The aliases are deliberately
// meaningless so a broader rule added later has nothing to match.
export const posthogScriptAliases: Record<string, string> = {
'posthog-recorder': 'ext-a',
'dead-clicks-autocapture': 'ext-b',
};
const staticScript = /\/static\/([a-z0-9-]+)\.js(?=\?|$)/;
// Rewrites a PostHog extension script URL to its alias, or returns it
// unchanged when the file is not on the list.
export const aliasPosthogScript = (src: string) =>
src.replace(staticScript, (match, name: string) =>
name in posthogScriptAliases
? `/static/${posthogScriptAliases[name]}.js`
: match,
);
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,710 @@
import { MigrateDownArgs, MigrateUpArgs, sql } from '@payloadcms/db-postgres';
export const up = async ({
db,
payload,
req,
}: MigrateUpArgs): Promise<void> => {
await db.execute(sql`
CREATE TYPE "public"."enum_landing_page_status" AS ENUM('draft', 'published');
CREATE TYPE "public"."enum__landing_page_v_version_status" AS ENUM('draft', 'published');
CREATE TABLE "users_sessions" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"created_at" timestamp(3) with time zone,
"expires_at" timestamp(3) with time zone NOT NULL
);
CREATE TABLE "users" (
"id" serial PRIMARY KEY NOT NULL,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"email" varchar NOT NULL,
"reset_password_token" varchar,
"reset_password_expiration" timestamp(3) with time zone,
"salt" varchar,
"hash" varchar,
"reset_password_requested_at" timestamp(3) with time zone,
"login_attempts" numeric DEFAULT 0,
"lock_until" timestamp(3) with time zone
);
CREATE TABLE "payload_kv" (
"id" serial PRIMARY KEY NOT NULL,
"key" varchar NOT NULL,
"data" jsonb NOT NULL
);
CREATE TABLE "payload_locked_documents" (
"id" serial PRIMARY KEY NOT NULL,
"global_slug" varchar,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "payload_locked_documents_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"users_id" integer
);
CREATE TABLE "payload_preferences" (
"id" serial PRIMARY KEY NOT NULL,
"key" varchar,
"value" jsonb,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "payload_preferences_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"users_id" integer
);
CREATE TABLE "payload_migrations" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar,
"batch" numeric,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "landing_page_blocks_hero_highlights" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"label" varchar
);
CREATE TABLE "landing_page_blocks_hero" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"badge_emoji" varchar DEFAULT '🚀',
"badge_text" varchar DEFAULT 'Production-ready monorepo template',
"heading_prefix" varchar DEFAULT 'Build Full-Stack Apps with',
"heading_highlight" varchar DEFAULT 'convex-monorepo',
"description" varchar DEFAULT 'A Turborepo starter with Next.js, Expo, and self-hosted Convex. Ship web and mobile apps faster with shared code, type-safe backend, and complete control over your infrastructure.',
"primary_cta_label" varchar DEFAULT 'View Source Code',
"primary_cta_url" varchar DEFAULT 'https://git.gbrown.org/gib/convex-monorepo',
"block_name" varchar
);
CREATE TABLE "landing_page_blocks_logo_cloud_items" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"name" varchar,
"accent" varchar
);
CREATE TABLE "landing_page_blocks_logo_cloud" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Designed For Modern Product Teams',
"block_name" varchar
);
CREATE TABLE "landing_page_blocks_features_items" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"icon" varchar,
"title" varchar,
"description" varchar
);
CREATE TABLE "landing_page_blocks_features" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Everything You Need to Ship Fast',
"description" varchar DEFAULT 'A complete monorepo template with all the tools and patterns you need for production-ready applications.',
"block_name" varchar
);
CREATE TABLE "landing_page_blocks_stats_items" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"value" varchar,
"label" varchar,
"description" varchar
);
CREATE TABLE "landing_page_blocks_stats" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Built To Demo Well',
"description" varchar DEFAULT 'These are the kinds of outcomes you can point to when showing the template to clients.',
"block_name" varchar
);
CREATE TABLE "landing_page_blocks_tech_stack_categories_technologies" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"name" varchar,
"description" varchar
);
CREATE TABLE "landing_page_blocks_tech_stack_categories" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"category" varchar
);
CREATE TABLE "landing_page_blocks_tech_stack" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Modern Tech Stack',
"description" varchar DEFAULT 'Built with the latest and greatest tools for maximum productivity and performance.',
"block_name" varchar
);
CREATE TABLE "landing_page_blocks_testimonials_items" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"quote" varchar,
"name" varchar,
"role" varchar,
"company" varchar,
"avatar_emoji" varchar
);
CREATE TABLE "landing_page_blocks_testimonials" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'What Clients Actually Care About',
"description" varchar DEFAULT 'The polished part is not just the frontend. Editors can shape the story, launch updates fast, and preview changes before they go live.',
"block_name" varchar
);
CREATE TABLE "landing_page_blocks_pricing_plans_features" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"label" varchar
);
CREATE TABLE "landing_page_blocks_pricing_plans" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"name" varchar,
"price" varchar,
"billing_period" varchar,
"description" varchar,
"badge" varchar,
"is_highlighted" boolean,
"cta_label" varchar,
"cta_url" varchar
);
CREATE TABLE "landing_page_blocks_pricing" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'A Great Way To Pitch Productized Delivery',
"description" varchar DEFAULT 'Use pricing blocks to show packaged service tiers, subscriptions, or implementation options right inside the landing page builder.',
"block_name" varchar
);
CREATE TABLE "landing_page_blocks_faq_items" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"question" varchar,
"answer" varchar
);
CREATE TABLE "landing_page_blocks_faq" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Questions You Will Probably Get In A Demo',
"description" varchar DEFAULT 'A good FAQ block makes the editor feel complete and gives clients another obvious content type they can manage themselves.',
"block_name" varchar
);
CREATE TABLE "landing_page_blocks_cta" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Ready to Build Something Amazing?',
"description" varchar DEFAULT 'Clone the repository and start building your next project with everything pre-configured.',
"command_label" varchar DEFAULT 'Quick Start',
"command" varchar DEFAULT 'git clone https://git.gbrown.org/gib/convex-monorepo.git
cd convex-monorepo
bun i',
"block_name" varchar
);
CREATE TABLE "landing_page" (
"id" serial PRIMARY KEY NOT NULL,
"_status" "enum_landing_page_status" DEFAULT 'draft',
"updated_at" timestamp(3) with time zone,
"created_at" timestamp(3) with time zone
);
CREATE TABLE "_landing_page_v_blocks_hero_highlights" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"label" varchar,
"_uuid" varchar
);
CREATE TABLE "_landing_page_v_blocks_hero" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"badge_emoji" varchar DEFAULT '🚀',
"badge_text" varchar DEFAULT 'Production-ready monorepo template',
"heading_prefix" varchar DEFAULT 'Build Full-Stack Apps with',
"heading_highlight" varchar DEFAULT 'convex-monorepo',
"description" varchar DEFAULT 'A Turborepo starter with Next.js, Expo, and self-hosted Convex. Ship web and mobile apps faster with shared code, type-safe backend, and complete control over your infrastructure.',
"primary_cta_label" varchar DEFAULT 'View Source Code',
"primary_cta_url" varchar DEFAULT 'https://git.gbrown.org/gib/convex-monorepo',
"_uuid" varchar,
"block_name" varchar
);
CREATE TABLE "_landing_page_v_blocks_logo_cloud_items" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"name" varchar,
"accent" varchar,
"_uuid" varchar
);
CREATE TABLE "_landing_page_v_blocks_logo_cloud" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Designed For Modern Product Teams',
"_uuid" varchar,
"block_name" varchar
);
CREATE TABLE "_landing_page_v_blocks_features_items" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"icon" varchar,
"title" varchar,
"description" varchar,
"_uuid" varchar
);
CREATE TABLE "_landing_page_v_blocks_features" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Everything You Need to Ship Fast',
"description" varchar DEFAULT 'A complete monorepo template with all the tools and patterns you need for production-ready applications.',
"_uuid" varchar,
"block_name" varchar
);
CREATE TABLE "_landing_page_v_blocks_stats_items" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"value" varchar,
"label" varchar,
"description" varchar,
"_uuid" varchar
);
CREATE TABLE "_landing_page_v_blocks_stats" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Built To Demo Well',
"description" varchar DEFAULT 'These are the kinds of outcomes you can point to when showing the template to clients.',
"_uuid" varchar,
"block_name" varchar
);
CREATE TABLE "_landing_page_v_blocks_tech_stack_categories_technologies" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"name" varchar,
"description" varchar,
"_uuid" varchar
);
CREATE TABLE "_landing_page_v_blocks_tech_stack_categories" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"category" varchar,
"_uuid" varchar
);
CREATE TABLE "_landing_page_v_blocks_tech_stack" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Modern Tech Stack',
"description" varchar DEFAULT 'Built with the latest and greatest tools for maximum productivity and performance.',
"_uuid" varchar,
"block_name" varchar
);
CREATE TABLE "_landing_page_v_blocks_testimonials_items" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"quote" varchar,
"name" varchar,
"role" varchar,
"company" varchar,
"avatar_emoji" varchar,
"_uuid" varchar
);
CREATE TABLE "_landing_page_v_blocks_testimonials" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'What Clients Actually Care About',
"description" varchar DEFAULT 'The polished part is not just the frontend. Editors can shape the story, launch updates fast, and preview changes before they go live.',
"_uuid" varchar,
"block_name" varchar
);
CREATE TABLE "_landing_page_v_blocks_pricing_plans_features" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"label" varchar,
"_uuid" varchar
);
CREATE TABLE "_landing_page_v_blocks_pricing_plans" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"name" varchar,
"price" varchar,
"billing_period" varchar,
"description" varchar,
"badge" varchar,
"is_highlighted" boolean,
"cta_label" varchar,
"cta_url" varchar,
"_uuid" varchar
);
CREATE TABLE "_landing_page_v_blocks_pricing" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'A Great Way To Pitch Productized Delivery',
"description" varchar DEFAULT 'Use pricing blocks to show packaged service tiers, subscriptions, or implementation options right inside the landing page builder.',
"_uuid" varchar,
"block_name" varchar
);
CREATE TABLE "_landing_page_v_blocks_faq_items" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"question" varchar,
"answer" varchar,
"_uuid" varchar
);
CREATE TABLE "_landing_page_v_blocks_faq" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Questions You Will Probably Get In A Demo',
"description" varchar DEFAULT 'A good FAQ block makes the editor feel complete and gives clients another obvious content type they can manage themselves.',
"_uuid" varchar,
"block_name" varchar
);
CREATE TABLE "_landing_page_v_blocks_cta" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" serial PRIMARY KEY NOT NULL,
"heading" varchar DEFAULT 'Ready to Build Something Amazing?',
"description" varchar DEFAULT 'Clone the repository and start building your next project with everything pre-configured.',
"command_label" varchar DEFAULT 'Quick Start',
"command" varchar DEFAULT 'git clone https://git.gbrown.org/gib/convex-monorepo.git
cd convex-monorepo
bun i',
"_uuid" varchar,
"block_name" varchar
);
CREATE TABLE "_landing_page_v" (
"id" serial PRIMARY KEY NOT NULL,
"version__status" "enum__landing_page_v_version_status" DEFAULT 'draft',
"version_updated_at" timestamp(3) with time zone,
"version_created_at" timestamp(3) with time zone,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"latest" boolean,
"autosave" boolean
);
ALTER TABLE "users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_locked_documents"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_preferences"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_hero_highlights" ADD CONSTRAINT "landing_page_blocks_hero_highlights_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page_blocks_hero"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_hero" ADD CONSTRAINT "landing_page_blocks_hero_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_logo_cloud_items" ADD CONSTRAINT "landing_page_blocks_logo_cloud_items_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page_blocks_logo_cloud"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_logo_cloud" ADD CONSTRAINT "landing_page_blocks_logo_cloud_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_features_items" ADD CONSTRAINT "landing_page_blocks_features_items_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page_blocks_features"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_features" ADD CONSTRAINT "landing_page_blocks_features_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_stats_items" ADD CONSTRAINT "landing_page_blocks_stats_items_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page_blocks_stats"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_stats" ADD CONSTRAINT "landing_page_blocks_stats_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_tech_stack_categories_technologies" ADD CONSTRAINT "landing_page_blocks_tech_stack_categories_technologies_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page_blocks_tech_stack_categories"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_tech_stack_categories" ADD CONSTRAINT "landing_page_blocks_tech_stack_categories_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page_blocks_tech_stack"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_tech_stack" ADD CONSTRAINT "landing_page_blocks_tech_stack_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_testimonials_items" ADD CONSTRAINT "landing_page_blocks_testimonials_items_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page_blocks_testimonials"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_testimonials" ADD CONSTRAINT "landing_page_blocks_testimonials_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_pricing_plans_features" ADD CONSTRAINT "landing_page_blocks_pricing_plans_features_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page_blocks_pricing_plans"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_pricing_plans" ADD CONSTRAINT "landing_page_blocks_pricing_plans_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page_blocks_pricing"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_pricing" ADD CONSTRAINT "landing_page_blocks_pricing_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_faq_items" ADD CONSTRAINT "landing_page_blocks_faq_items_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page_blocks_faq"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_faq" ADD CONSTRAINT "landing_page_blocks_faq_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "landing_page_blocks_cta" ADD CONSTRAINT "landing_page_blocks_cta_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."landing_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_hero_highlights" ADD CONSTRAINT "_landing_page_v_blocks_hero_highlights_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v_blocks_hero"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_hero" ADD CONSTRAINT "_landing_page_v_blocks_hero_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_logo_cloud_items" ADD CONSTRAINT "_landing_page_v_blocks_logo_cloud_items_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v_blocks_logo_cloud"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_logo_cloud" ADD CONSTRAINT "_landing_page_v_blocks_logo_cloud_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_features_items" ADD CONSTRAINT "_landing_page_v_blocks_features_items_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v_blocks_features"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_features" ADD CONSTRAINT "_landing_page_v_blocks_features_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_stats_items" ADD CONSTRAINT "_landing_page_v_blocks_stats_items_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v_blocks_stats"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_stats" ADD CONSTRAINT "_landing_page_v_blocks_stats_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_tech_stack_categories_technologies" ADD CONSTRAINT "_landing_page_v_blocks_tech_stack_categories_technologies_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v_blocks_tech_stack_categories"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_tech_stack_categories" ADD CONSTRAINT "_landing_page_v_blocks_tech_stack_categories_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v_blocks_tech_stack"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_tech_stack" ADD CONSTRAINT "_landing_page_v_blocks_tech_stack_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_testimonials_items" ADD CONSTRAINT "_landing_page_v_blocks_testimonials_items_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v_blocks_testimonials"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_testimonials" ADD CONSTRAINT "_landing_page_v_blocks_testimonials_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_pricing_plans_features" ADD CONSTRAINT "_landing_page_v_blocks_pricing_plans_features_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v_blocks_pricing_plans"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_pricing_plans" ADD CONSTRAINT "_landing_page_v_blocks_pricing_plans_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v_blocks_pricing"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_pricing" ADD CONSTRAINT "_landing_page_v_blocks_pricing_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_faq_items" ADD CONSTRAINT "_landing_page_v_blocks_faq_items_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v_blocks_faq"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_faq" ADD CONSTRAINT "_landing_page_v_blocks_faq_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "_landing_page_v_blocks_cta" ADD CONSTRAINT "_landing_page_v_blocks_cta_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."_landing_page_v"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "users_sessions_order_idx" ON "users_sessions" USING btree ("_order");
CREATE INDEX "users_sessions_parent_id_idx" ON "users_sessions" USING btree ("_parent_id");
CREATE INDEX "users_updated_at_idx" ON "users" USING btree ("updated_at");
CREATE INDEX "users_created_at_idx" ON "users" USING btree ("created_at");
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");
CREATE UNIQUE INDEX "payload_kv_key_idx" ON "payload_kv" USING btree ("key");
CREATE INDEX "payload_locked_documents_global_slug_idx" ON "payload_locked_documents" USING btree ("global_slug");
CREATE INDEX "payload_locked_documents_updated_at_idx" ON "payload_locked_documents" USING btree ("updated_at");
CREATE INDEX "payload_locked_documents_created_at_idx" ON "payload_locked_documents" USING btree ("created_at");
CREATE INDEX "payload_locked_documents_rels_order_idx" ON "payload_locked_documents_rels" USING btree ("order");
CREATE INDEX "payload_locked_documents_rels_parent_idx" ON "payload_locked_documents_rels" USING btree ("parent_id");
CREATE INDEX "payload_locked_documents_rels_path_idx" ON "payload_locked_documents_rels" USING btree ("path");
CREATE INDEX "payload_locked_documents_rels_users_id_idx" ON "payload_locked_documents_rels" USING btree ("users_id");
CREATE INDEX "payload_preferences_key_idx" ON "payload_preferences" USING btree ("key");
CREATE INDEX "payload_preferences_updated_at_idx" ON "payload_preferences" USING btree ("updated_at");
CREATE INDEX "payload_preferences_created_at_idx" ON "payload_preferences" USING btree ("created_at");
CREATE INDEX "payload_preferences_rels_order_idx" ON "payload_preferences_rels" USING btree ("order");
CREATE INDEX "payload_preferences_rels_parent_idx" ON "payload_preferences_rels" USING btree ("parent_id");
CREATE INDEX "payload_preferences_rels_path_idx" ON "payload_preferences_rels" USING btree ("path");
CREATE INDEX "payload_preferences_rels_users_id_idx" ON "payload_preferences_rels" USING btree ("users_id");
CREATE INDEX "payload_migrations_updated_at_idx" ON "payload_migrations" USING btree ("updated_at");
CREATE INDEX "payload_migrations_created_at_idx" ON "payload_migrations" USING btree ("created_at");
CREATE INDEX "landing_page_blocks_hero_highlights_order_idx" ON "landing_page_blocks_hero_highlights" USING btree ("_order");
CREATE INDEX "landing_page_blocks_hero_highlights_parent_id_idx" ON "landing_page_blocks_hero_highlights" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_hero_order_idx" ON "landing_page_blocks_hero" USING btree ("_order");
CREATE INDEX "landing_page_blocks_hero_parent_id_idx" ON "landing_page_blocks_hero" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_hero_path_idx" ON "landing_page_blocks_hero" USING btree ("_path");
CREATE INDEX "landing_page_blocks_logo_cloud_items_order_idx" ON "landing_page_blocks_logo_cloud_items" USING btree ("_order");
CREATE INDEX "landing_page_blocks_logo_cloud_items_parent_id_idx" ON "landing_page_blocks_logo_cloud_items" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_logo_cloud_order_idx" ON "landing_page_blocks_logo_cloud" USING btree ("_order");
CREATE INDEX "landing_page_blocks_logo_cloud_parent_id_idx" ON "landing_page_blocks_logo_cloud" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_logo_cloud_path_idx" ON "landing_page_blocks_logo_cloud" USING btree ("_path");
CREATE INDEX "landing_page_blocks_features_items_order_idx" ON "landing_page_blocks_features_items" USING btree ("_order");
CREATE INDEX "landing_page_blocks_features_items_parent_id_idx" ON "landing_page_blocks_features_items" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_features_order_idx" ON "landing_page_blocks_features" USING btree ("_order");
CREATE INDEX "landing_page_blocks_features_parent_id_idx" ON "landing_page_blocks_features" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_features_path_idx" ON "landing_page_blocks_features" USING btree ("_path");
CREATE INDEX "landing_page_blocks_stats_items_order_idx" ON "landing_page_blocks_stats_items" USING btree ("_order");
CREATE INDEX "landing_page_blocks_stats_items_parent_id_idx" ON "landing_page_blocks_stats_items" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_stats_order_idx" ON "landing_page_blocks_stats" USING btree ("_order");
CREATE INDEX "landing_page_blocks_stats_parent_id_idx" ON "landing_page_blocks_stats" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_stats_path_idx" ON "landing_page_blocks_stats" USING btree ("_path");
CREATE INDEX "landing_page_blocks_tech_stack_categories_technologies_order_idx" ON "landing_page_blocks_tech_stack_categories_technologies" USING btree ("_order");
CREATE INDEX "landing_page_blocks_tech_stack_categories_technologies_parent_id_idx" ON "landing_page_blocks_tech_stack_categories_technologies" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_tech_stack_categories_order_idx" ON "landing_page_blocks_tech_stack_categories" USING btree ("_order");
CREATE INDEX "landing_page_blocks_tech_stack_categories_parent_id_idx" ON "landing_page_blocks_tech_stack_categories" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_tech_stack_order_idx" ON "landing_page_blocks_tech_stack" USING btree ("_order");
CREATE INDEX "landing_page_blocks_tech_stack_parent_id_idx" ON "landing_page_blocks_tech_stack" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_tech_stack_path_idx" ON "landing_page_blocks_tech_stack" USING btree ("_path");
CREATE INDEX "landing_page_blocks_testimonials_items_order_idx" ON "landing_page_blocks_testimonials_items" USING btree ("_order");
CREATE INDEX "landing_page_blocks_testimonials_items_parent_id_idx" ON "landing_page_blocks_testimonials_items" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_testimonials_order_idx" ON "landing_page_blocks_testimonials" USING btree ("_order");
CREATE INDEX "landing_page_blocks_testimonials_parent_id_idx" ON "landing_page_blocks_testimonials" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_testimonials_path_idx" ON "landing_page_blocks_testimonials" USING btree ("_path");
CREATE INDEX "landing_page_blocks_pricing_plans_features_order_idx" ON "landing_page_blocks_pricing_plans_features" USING btree ("_order");
CREATE INDEX "landing_page_blocks_pricing_plans_features_parent_id_idx" ON "landing_page_blocks_pricing_plans_features" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_pricing_plans_order_idx" ON "landing_page_blocks_pricing_plans" USING btree ("_order");
CREATE INDEX "landing_page_blocks_pricing_plans_parent_id_idx" ON "landing_page_blocks_pricing_plans" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_pricing_order_idx" ON "landing_page_blocks_pricing" USING btree ("_order");
CREATE INDEX "landing_page_blocks_pricing_parent_id_idx" ON "landing_page_blocks_pricing" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_pricing_path_idx" ON "landing_page_blocks_pricing" USING btree ("_path");
CREATE INDEX "landing_page_blocks_faq_items_order_idx" ON "landing_page_blocks_faq_items" USING btree ("_order");
CREATE INDEX "landing_page_blocks_faq_items_parent_id_idx" ON "landing_page_blocks_faq_items" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_faq_order_idx" ON "landing_page_blocks_faq" USING btree ("_order");
CREATE INDEX "landing_page_blocks_faq_parent_id_idx" ON "landing_page_blocks_faq" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_faq_path_idx" ON "landing_page_blocks_faq" USING btree ("_path");
CREATE INDEX "landing_page_blocks_cta_order_idx" ON "landing_page_blocks_cta" USING btree ("_order");
CREATE INDEX "landing_page_blocks_cta_parent_id_idx" ON "landing_page_blocks_cta" USING btree ("_parent_id");
CREATE INDEX "landing_page_blocks_cta_path_idx" ON "landing_page_blocks_cta" USING btree ("_path");
CREATE INDEX "landing_page__status_idx" ON "landing_page" USING btree ("_status");
CREATE INDEX "_landing_page_v_blocks_hero_highlights_order_idx" ON "_landing_page_v_blocks_hero_highlights" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_hero_highlights_parent_id_idx" ON "_landing_page_v_blocks_hero_highlights" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_hero_order_idx" ON "_landing_page_v_blocks_hero" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_hero_parent_id_idx" ON "_landing_page_v_blocks_hero" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_hero_path_idx" ON "_landing_page_v_blocks_hero" USING btree ("_path");
CREATE INDEX "_landing_page_v_blocks_logo_cloud_items_order_idx" ON "_landing_page_v_blocks_logo_cloud_items" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_logo_cloud_items_parent_id_idx" ON "_landing_page_v_blocks_logo_cloud_items" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_logo_cloud_order_idx" ON "_landing_page_v_blocks_logo_cloud" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_logo_cloud_parent_id_idx" ON "_landing_page_v_blocks_logo_cloud" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_logo_cloud_path_idx" ON "_landing_page_v_blocks_logo_cloud" USING btree ("_path");
CREATE INDEX "_landing_page_v_blocks_features_items_order_idx" ON "_landing_page_v_blocks_features_items" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_features_items_parent_id_idx" ON "_landing_page_v_blocks_features_items" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_features_order_idx" ON "_landing_page_v_blocks_features" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_features_parent_id_idx" ON "_landing_page_v_blocks_features" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_features_path_idx" ON "_landing_page_v_blocks_features" USING btree ("_path");
CREATE INDEX "_landing_page_v_blocks_stats_items_order_idx" ON "_landing_page_v_blocks_stats_items" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_stats_items_parent_id_idx" ON "_landing_page_v_blocks_stats_items" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_stats_order_idx" ON "_landing_page_v_blocks_stats" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_stats_parent_id_idx" ON "_landing_page_v_blocks_stats" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_stats_path_idx" ON "_landing_page_v_blocks_stats" USING btree ("_path");
CREATE INDEX "_landing_page_v_blocks_tech_stack_categories_technologies_order_idx" ON "_landing_page_v_blocks_tech_stack_categories_technologies" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_tech_stack_categories_technologies_parent_id_idx" ON "_landing_page_v_blocks_tech_stack_categories_technologies" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_tech_stack_categories_order_idx" ON "_landing_page_v_blocks_tech_stack_categories" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_tech_stack_categories_parent_id_idx" ON "_landing_page_v_blocks_tech_stack_categories" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_tech_stack_order_idx" ON "_landing_page_v_blocks_tech_stack" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_tech_stack_parent_id_idx" ON "_landing_page_v_blocks_tech_stack" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_tech_stack_path_idx" ON "_landing_page_v_blocks_tech_stack" USING btree ("_path");
CREATE INDEX "_landing_page_v_blocks_testimonials_items_order_idx" ON "_landing_page_v_blocks_testimonials_items" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_testimonials_items_parent_id_idx" ON "_landing_page_v_blocks_testimonials_items" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_testimonials_order_idx" ON "_landing_page_v_blocks_testimonials" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_testimonials_parent_id_idx" ON "_landing_page_v_blocks_testimonials" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_testimonials_path_idx" ON "_landing_page_v_blocks_testimonials" USING btree ("_path");
CREATE INDEX "_landing_page_v_blocks_pricing_plans_features_order_idx" ON "_landing_page_v_blocks_pricing_plans_features" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_pricing_plans_features_parent_id_idx" ON "_landing_page_v_blocks_pricing_plans_features" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_pricing_plans_order_idx" ON "_landing_page_v_blocks_pricing_plans" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_pricing_plans_parent_id_idx" ON "_landing_page_v_blocks_pricing_plans" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_pricing_order_idx" ON "_landing_page_v_blocks_pricing" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_pricing_parent_id_idx" ON "_landing_page_v_blocks_pricing" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_pricing_path_idx" ON "_landing_page_v_blocks_pricing" USING btree ("_path");
CREATE INDEX "_landing_page_v_blocks_faq_items_order_idx" ON "_landing_page_v_blocks_faq_items" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_faq_items_parent_id_idx" ON "_landing_page_v_blocks_faq_items" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_faq_order_idx" ON "_landing_page_v_blocks_faq" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_faq_parent_id_idx" ON "_landing_page_v_blocks_faq" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_faq_path_idx" ON "_landing_page_v_blocks_faq" USING btree ("_path");
CREATE INDEX "_landing_page_v_blocks_cta_order_idx" ON "_landing_page_v_blocks_cta" USING btree ("_order");
CREATE INDEX "_landing_page_v_blocks_cta_parent_id_idx" ON "_landing_page_v_blocks_cta" USING btree ("_parent_id");
CREATE INDEX "_landing_page_v_blocks_cta_path_idx" ON "_landing_page_v_blocks_cta" USING btree ("_path");
CREATE INDEX "_landing_page_v_version_version__status_idx" ON "_landing_page_v" USING btree ("version__status");
CREATE INDEX "_landing_page_v_created_at_idx" ON "_landing_page_v" USING btree ("created_at");
CREATE INDEX "_landing_page_v_updated_at_idx" ON "_landing_page_v" USING btree ("updated_at");
CREATE INDEX "_landing_page_v_latest_idx" ON "_landing_page_v" USING btree ("latest");
CREATE INDEX "_landing_page_v_autosave_idx" ON "_landing_page_v" USING btree ("autosave");`);
};
export const down = async ({
db,
payload,
req,
}: MigrateDownArgs): Promise<void> => {
await db.execute(sql`
DROP TABLE "users_sessions" CASCADE;
DROP TABLE "users" CASCADE;
DROP TABLE "payload_kv" CASCADE;
DROP TABLE "payload_locked_documents" CASCADE;
DROP TABLE "payload_locked_documents_rels" CASCADE;
DROP TABLE "payload_preferences" CASCADE;
DROP TABLE "payload_preferences_rels" CASCADE;
DROP TABLE "payload_migrations" CASCADE;
DROP TABLE "landing_page_blocks_hero_highlights" CASCADE;
DROP TABLE "landing_page_blocks_hero" CASCADE;
DROP TABLE "landing_page_blocks_logo_cloud_items" CASCADE;
DROP TABLE "landing_page_blocks_logo_cloud" CASCADE;
DROP TABLE "landing_page_blocks_features_items" CASCADE;
DROP TABLE "landing_page_blocks_features" CASCADE;
DROP TABLE "landing_page_blocks_stats_items" CASCADE;
DROP TABLE "landing_page_blocks_stats" CASCADE;
DROP TABLE "landing_page_blocks_tech_stack_categories_technologies" CASCADE;
DROP TABLE "landing_page_blocks_tech_stack_categories" CASCADE;
DROP TABLE "landing_page_blocks_tech_stack" CASCADE;
DROP TABLE "landing_page_blocks_testimonials_items" CASCADE;
DROP TABLE "landing_page_blocks_testimonials" CASCADE;
DROP TABLE "landing_page_blocks_pricing_plans_features" CASCADE;
DROP TABLE "landing_page_blocks_pricing_plans" CASCADE;
DROP TABLE "landing_page_blocks_pricing" CASCADE;
DROP TABLE "landing_page_blocks_faq_items" CASCADE;
DROP TABLE "landing_page_blocks_faq" CASCADE;
DROP TABLE "landing_page_blocks_cta" CASCADE;
DROP TABLE "landing_page" CASCADE;
DROP TABLE "_landing_page_v_blocks_hero_highlights" CASCADE;
DROP TABLE "_landing_page_v_blocks_hero" CASCADE;
DROP TABLE "_landing_page_v_blocks_logo_cloud_items" CASCADE;
DROP TABLE "_landing_page_v_blocks_logo_cloud" CASCADE;
DROP TABLE "_landing_page_v_blocks_features_items" CASCADE;
DROP TABLE "_landing_page_v_blocks_features" CASCADE;
DROP TABLE "_landing_page_v_blocks_stats_items" CASCADE;
DROP TABLE "_landing_page_v_blocks_stats" CASCADE;
DROP TABLE "_landing_page_v_blocks_tech_stack_categories_technologies" CASCADE;
DROP TABLE "_landing_page_v_blocks_tech_stack_categories" CASCADE;
DROP TABLE "_landing_page_v_blocks_tech_stack" CASCADE;
DROP TABLE "_landing_page_v_blocks_testimonials_items" CASCADE;
DROP TABLE "_landing_page_v_blocks_testimonials" CASCADE;
DROP TABLE "_landing_page_v_blocks_pricing_plans_features" CASCADE;
DROP TABLE "_landing_page_v_blocks_pricing_plans" CASCADE;
DROP TABLE "_landing_page_v_blocks_pricing" CASCADE;
DROP TABLE "_landing_page_v_blocks_faq_items" CASCADE;
DROP TABLE "_landing_page_v_blocks_faq" CASCADE;
DROP TABLE "_landing_page_v_blocks_cta" CASCADE;
DROP TABLE "_landing_page_v" CASCADE;
DROP TYPE "public"."enum_landing_page_status";
DROP TYPE "public"."enum__landing_page_v_version_status";`);
};
+9
View File
@@ -0,0 +1,9 @@
import * as migration_20261001_183341_baseline from './20261001_183341_baseline';
export const migrations = [
{
up: migration_20261001_183341_baseline.up,
down: migration_20261001_183341_baseline.down,
name: '20261001_183341_baseline',
},
];
+10
View File
@@ -4,6 +4,7 @@ import { buildConfig } from 'payload';
import sharp from 'sharp';
import { env } from './env';
import { migrations } from './migrations';
import { Users } from './payload/collections/users';
import { LandingPage } from './payload/globals/landing-page';
@@ -17,6 +18,15 @@ export default buildConfig({
pool: {
connectionString: env.PAYLOAD_DB_URL,
},
// Schema changes ship as migrations (`bunx payload migrate:create <name>`
// from apps/next, through `bun with-env`). Production runs pending ones on
// startup. Dev push only ever touches a local database: a push against the
// production database would mark it as push-managed, and the next startup
// migration would stop on Payload's confirmation prompt.
push: ['localhost', '127.0.0.1'].includes(
new URL(env.PAYLOAD_DB_URL).hostname,
),
prodMigrations: migrations,
}),
sharp,
});
+30
View File
@@ -0,0 +1,30 @@
import type { env } from '@/env';
type PublicEnv = Pick<
typeof env,
| 'NEXT_PUBLIC_SITE_URL'
| 'NEXT_PUBLIC_CONVEX_URL'
| 'NEXT_PUBLIC_PLAUSIBLE_SRC'
| 'NEXT_PUBLIC_POSTHOG_KEY'
| 'NEXT_PUBLIC_POSTHOG_HOST'
| 'NEXT_PUBLIC_SENTRY_DSN'
>;
// Empty strings count as unset, matching `emptyStringAsUndefined` in env.ts.
const read = (value: string | undefined) => (value === '' ? undefined : value);
// The browser-side NEXT_PUBLIC_* values for client components and
// instrumentation-client. Next inlines each literal `process.env.X` at build
// time, and env.ts has already validated them by then (next.config.js imports
// it), so reading them here keeps zod and the env schema out of every
// visitor's first download. Server code keeps importing `@/env`.
/* eslint-disable no-restricted-properties -- the one sanctioned client-side reader, see AGENTS.md */
export const publicEnv = {
NEXT_PUBLIC_SITE_URL: read(process.env.NEXT_PUBLIC_SITE_URL),
NEXT_PUBLIC_CONVEX_URL: read(process.env.NEXT_PUBLIC_CONVEX_URL),
NEXT_PUBLIC_PLAUSIBLE_SRC: read(process.env.NEXT_PUBLIC_PLAUSIBLE_SRC),
NEXT_PUBLIC_POSTHOG_KEY: read(process.env.NEXT_PUBLIC_POSTHOG_KEY),
NEXT_PUBLIC_POSTHOG_HOST: read(process.env.NEXT_PUBLIC_POSTHOG_HOST),
NEXT_PUBLIC_SENTRY_DSN: read(process.env.NEXT_PUBLIC_SENTRY_DSN),
} as PublicEnv;
/* eslint-enable no-restricted-properties */
+2 -1
View File
@@ -3,7 +3,8 @@ import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1,
// One in ten requests keeps traces useful without loading the home server.
tracesSampleRate: 0.1,
enableLogs: true,
debug: false,
});
+661 -1229
View File
File diff suppressed because it is too large. Load diff
+9 -5
View File
@@ -1,16 +1,18 @@
# syntax=docker/dockerfile:1
FROM docker.io/oven/bun:1.3.10-alpine AS base
FROM docker.io/oven/bun:1.3.14-alpine AS base
# Builder stage
FROM base AS builder
RUN apk add --no-cache libc6-compat
RUN apk add --no-cache curl libc6-compat
WORKDIR /app
ARG SENTRY_AUTH_TOKEN
ARG SENTRY_DISABLE_AUTO_UPLOAD=false
ARG NEXT_PUBLIC_SITE_URL
ARG NEXT_PUBLIC_CONVEX_URL
ARG NEXT_PUBLIC_PLAUSIBLE_URL
ARG NEXT_PUBLIC_PLAUSIBLE_SRC
ARG NEXT_PUBLIC_POSTHOG_KEY
ARG NEXT_PUBLIC_POSTHOG_HOST
ARG NEXT_PUBLIC_SENTRY_DSN
ARG NEXT_PUBLIC_SENTRY_URL
ARG NEXT_PUBLIC_SENTRY_ORG
@@ -22,7 +24,9 @@ ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
ENV SENTRY_DISABLE_AUTO_UPLOAD=$SENTRY_DISABLE_AUTO_UPLOAD
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_CONVEX_URL=$NEXT_PUBLIC_CONVEX_URL
ENV NEXT_PUBLIC_PLAUSIBLE_URL=$NEXT_PUBLIC_PLAUSIBLE_URL
ENV NEXT_PUBLIC_PLAUSIBLE_SRC=$NEXT_PUBLIC_PLAUSIBLE_SRC
ENV NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY
ENV NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST
ENV NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN
ENV NEXT_PUBLIC_SENTRY_URL=$NEXT_PUBLIC_SENTRY_URL
ENV NEXT_PUBLIC_SENTRY_ORG=$NEXT_PUBLIC_SENTRY_ORG
@@ -40,7 +44,7 @@ RUN bun install --frozen-lockfile
# Build with proper environment
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN cd apps/next && bun run build:docker
RUN sh scripts/build-next-bundle
RUN bun scripts/copy-next-runtime-deps.mjs
# Runner stage
+7 -7
View File
@@ -4,7 +4,7 @@ services:
postgres:
image: postgres:17
container_name: convexmonorepo-local-postgres
ports: ['${POSTGRES_PORT:-5432}:5432']
ports: ['${POSTGRES_PORT:-5436}:5432']
environment:
- POSTGRES_USER=${POSTGRES_USER:-convexmonorepo}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-localdev}
@@ -22,13 +22,13 @@ services:
image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
container_name: convexmonorepo-local-convex
ports:
- '${BACKEND_PORT:-3210}:3210'
- '${SITE_PROXY_PORT:-3211}:3211'
- '${BACKEND_PORT:-3240}:3210'
- '${SITE_PROXY_PORT:-3241}:3211'
environment:
- INSTANCE_NAME=${LOCAL_INSTANCE_NAME:-convexmonorepo_local}
- INSTANCE_SECRET=${LOCAL_INSTANCE_SECRET:-0000000000000000000000000000000000000000000000000000000000000000}
- CONVEX_CLOUD_ORIGIN=http://localhost:${BACKEND_PORT:-3210}
- CONVEX_SITE_ORIGIN=http://localhost:${SITE_PROXY_PORT:-3211}
- CONVEX_CLOUD_ORIGIN=http://localhost:${BACKEND_PORT:-3240}
- CONVEX_SITE_ORIGIN=http://localhost:${SITE_PROXY_PORT:-3241}
- DISABLE_BEACON=true
- REDACT_LOGS_TO_CLIENT=false
- DO_NOT_REQUIRE_SSL=true
@@ -49,9 +49,9 @@ services:
convex-dashboard:
image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
container_name: convexmonorepo-local-convex-dashboard
ports: ['${DASHBOARD_PORT:-6791}:6791']
ports: ['${DASHBOARD_PORT:-6794}:6791']
environment:
- NEXT_PUBLIC_DEPLOYMENT_URL=http://localhost:${BACKEND_PORT:-3210}
- NEXT_PUBLIC_DEPLOYMENT_URL=http://localhost:${BACKEND_PORT:-3240}
depends_on:
convex-backend:
condition: service_healthy
+6 -2
View File
@@ -12,7 +12,9 @@ services:
SENTRY_DISABLE_AUTO_UPLOAD: ${SENTRY_DISABLE_AUTO_UPLOAD:-false}
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL}
NEXT_PUBLIC_CONVEX_URL: ${NEXT_PUBLIC_CONVEX_URL}
NEXT_PUBLIC_PLAUSIBLE_URL: ${NEXT_PUBLIC_PLAUSIBLE_URL}
NEXT_PUBLIC_PLAUSIBLE_SRC: ${NEXT_PUBLIC_PLAUSIBLE_SRC:-}
NEXT_PUBLIC_POSTHOG_KEY: ${NEXT_PUBLIC_POSTHOG_KEY:-}
NEXT_PUBLIC_POSTHOG_HOST: ${NEXT_PUBLIC_POSTHOG_HOST:-}
NEXT_PUBLIC_SENTRY_DSN: ${NEXT_PUBLIC_SENTRY_DSN}
NEXT_PUBLIC_SENTRY_URL: ${NEXT_PUBLIC_SENTRY_URL}
NEXT_PUBLIC_SENTRY_ORG: ${NEXT_PUBLIC_SENTRY_ORG}
@@ -27,7 +29,9 @@ services:
- SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL:-http://localhost:${NEXT_PORT:-3000}}
- NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL:-http://${BACKEND_CONTAINER_NAME:-convex-backend}:${BACKEND_PORT:-3210}}
- NEXT_PUBLIC_PLAUSIBLE_URL=${NEXT_PUBLIC_PLAUSIBLE_URL:-https://plausible.gbrown.org}
- NEXT_PUBLIC_PLAUSIBLE_SRC=${NEXT_PUBLIC_PLAUSIBLE_SRC:-}
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY:-}
- NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST:-}
- NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
- NEXT_PUBLIC_SENTRY_URL=${NEXT_PUBLIC_SENTRY_URL}
- NEXT_PUBLIC_SENTRY_ORG=${NEXT_PUBLIC_SENTRY_ORG:-sentry}
+1 -1
View File
@@ -6,7 +6,7 @@ the application backend for auth, realtime data, files, and workflows.
Configuration lives in `apps/next/src/payload.config.ts`. Payload uses
`PAYLOAD_SECRET`, `PAYLOAD_DB_URL`, and `NEXT_PUBLIC_SITE_URL` from the validated
environment contract. Local `dev` values come from Infisical and point Payload
to Postgres at `localhost:5432`, because Next runs on the host.
to Postgres at `localhost:5436`, because Next runs on the host.
Start the isolated services with `bun db:up`, then run `bun dev:next`. The local
database persists across `bun db:down`; `bun db:down:wipe` removes it. Local
+16 -14
View File
@@ -4,7 +4,7 @@
"engines": {
"node": "^22.20.0"
},
"packageManager": "[email protected]0",
"packageManager": "[email protected]4",
"workspaces": [
"apps/*",
"packages/*",
@@ -25,24 +25,24 @@
"test": {
"@edge-runtime/vm": "^5.0.0",
"@playwright/test": "^1.60.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/jest-dom": "^6.10.0",
"@testing-library/react": "^16.3.3",
"@testing-library/user-event": "^14.6.1",
"@vitejs/plugin-react": "^6.0.2",
"@vitejs/plugin-react": "^6.1.1",
"@vitest/coverage-v8": "^4.1.8",
"convex-test": "^0.0.53",
"jsdom": "^29.1.1",
"vitest": "^4.1.8"
"vitest": "^4.1.11"
},
"convex": {
"@convex-dev/auth": "^0.0.87",
"convex": "^1.34.1"
"@convex-dev/auth": "^0.0.96",
"convex": "^1.46.0"
},
"react19": {
"@types/react": "~19.2.14",
"@types/react-dom": "~19.2.3",
"react": "19.2.4",
"react-dom": "19.2.4"
"@types/react": "~19.3.0",
"@types/react-dom": "~19.3.0",
"react": "19.3.0",
"react-dom": "19.3.0"
}
},
"scripts": {
@@ -55,11 +55,13 @@
"dev:next:web": "turbo run dev:web -F @gib/next -F @gib/backend",
"dev:expo": "turbo run dev -F @gib/expo -F @gib/backend",
"dev:backend": "turbo run dev -F @gib/backend",
"dev:kill": "bash scripts/dev-kill",
"dev:staging": "INFISICAL_ENV=staging turbo run dev -F @gib/next -F @gib/backend",
"dev:expo:tunnel": "turbo run dev:tunnel -F @gib/expo -F @gib/backend",
"db:up": "bash scripts/db/up",
"db:down": "bash scripts/db/down",
"db:down:wipe": "bash scripts/db/down --wipe",
"db:sync-env": "bash scripts/with-env dev -- bash scripts/sync-local-convex-env",
"db:seed:payload": "bash scripts/db/seed-payload",
"db:sync:payload": "bash scripts/db/sync-payload",
"format": "turbo run format --continue -- --cache --cache-location .cache/.prettiercache",
@@ -84,12 +86,12 @@
},
"devDependencies": {
"@gib/prettier-config": "workspace:",
"@turbo/gen": "^2.9.18",
"@turbo/gen": "^2.11.6",
"dotenv-cli": "11.0.0",
"husky": "^9.1.7",
"lint-staged": "^17.0.7",
"lint-staged": "^17.6.0",
"prettier": "catalog:",
"turbo": "^2.9.18",
"turbo": "^2.11.6",
"typescript": "catalog:"
},
"prettier": "@gib/prettier-config",
+19
View File
@@ -21,6 +21,17 @@ import {
} from "convex/server";
import type { DataModel } from "./dataModel.js";
/**
* Typesafe environment variables.
*
* This includes platform-provided env vars and any variables declared in
* `convex.config.ts`.
*/
type Env = {
readonly CONVEX_CLOUD_URL: string;
readonly CONVEX_SITE_URL: string;
};
/**
* Define a query in this Convex app's public API.
*
@@ -95,6 +106,14 @@ export declare const internalAction: ActionBuilder<DataModel, "internal">;
*/
export declare const httpAction: HttpActionBuilder;
/**
* Typesafe environment variables.
*
* This includes platform-provided env vars and any variables declared in
* `convex.config.ts`.
*/
export declare const env: Env;
/**
* A set of services for use within Convex query functions.
*
@@ -91,3 +91,11 @@ export const internalAction = internalActionGeneric;
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
*/
export const httpAction = httpActionGeneric;
/**
* Typesafe environment variables.
*
* This includes platform-provided env vars and any variables declared in
* `convex.config.ts`.
*/
export const env = process.env;
@@ -1,6 +1,5 @@
import type { EmailConfig, EmailUserConfig } from '@auth/core/providers/email';
import { generateRandomString, RandomReader } from '@oslojs/crypto/random';
import { alphabet } from 'oslo/crypto';
import { UseSend } from 'usesend-js';
export default function UseSendProvider(config: EmailUserConfig): EmailConfig {
@@ -17,7 +16,7 @@ export default function UseSendProvider(config: EmailUserConfig): EmailConfig {
crypto.getRandomValues(bytes as Uint8Array<ArrayBuffer>);
},
};
return generateRandomString(random, alphabet('0-9'), 6);
return generateRandomString(random, '0123456789', 6);
},
sendVerificationRequest: async (params) => {
+3 -2
View File
@@ -26,9 +26,10 @@
"with-env": "sh ../../scripts/with-env ${INFISICAL_ENV:-dev} --"
},
"dependencies": {
"@auth/core": "^0.41.3",
"@oslojs/crypto": "^1.0.1",
"@react-email/components": "1.0.10",
"@react-email/render": "^2.0.4",
"@react-email/components": "1.0.12",
"@react-email/render": "^2.1.0",
"convex": "catalog:convex",
"react": "catalog:react19",
"react-dom": "catalog:react19",
+24 -24
View File
@@ -18,38 +18,38 @@
"ui-add": "bunx --bun shadcn@latest add && prettier src --write --list-different"
},
"dependencies": {
"@base-ui/react": "^1.3.0",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@base-ui/react": "^1.8.0",
"@hookform/resolvers": "^5.9.1",
"@radix-ui/react-avatar": "^1.2.6",
"@radix-ui/react-checkbox": "^1.3.11",
"@radix-ui/react-dialog": "^1.1.23",
"@radix-ui/react-dropdown-menu": "^2.1.24",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@tabler/icons-react": "^3.41.0",
"@radix-ui/react-label": "^2.1.15",
"@radix-ui/react-progress": "^1.1.16",
"@radix-ui/react-scroll-area": "^1.2.18",
"@radix-ui/react-separator": "^1.1.15",
"@radix-ui/react-slot": "^1.3.3",
"@radix-ui/react-switch": "^1.3.7",
"@radix-ui/react-tabs": "^1.1.21",
"@tabler/icons-react": "^3.48.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"date-fns": "^4.4.0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"input-otp": "^1.5.0",
"lucide-react": "^0.577.0",
"motion": "^12.38.0",
"motion": "^12.43.0",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"radix-ui": "^1.6.7",
"react-day-picker": "^9.14.0",
"react-hook-form": "^7.72.0",
"react-image-crop": "^11.0.10",
"react-resizable-panels": "^4.7.6",
"recharts": "^3.8.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"react-hook-form": "^7.89.0",
"react-image-crop": "^11.1.2",
"react-resizable-panels": "^4.14.1",
"recharts": "^3.10.1",
"sonner": "^2.0.8",
"tailwind-merge": "^3.7.0",
"vaul": "^1.1.2"
},
"devDependencies": {
+3
View File
@@ -93,6 +93,9 @@ const Carousel = ({
React.useEffect(() => {
if (!api) return;
// Embla is the source of truth for scroll state; read it once on mount,
// then follow its events.
// eslint-disable-next-line react-hooks/set-state-in-effect
onSelect(api);
api.on('reInit', onSelect);
api.on('select', onSelect);
+6 -4
View File
@@ -105,6 +105,10 @@ ${colorConfig
const ChartTooltip = RechartsPrimitive.Tooltip;
// Recharts injects `payload`/`label`/`active` into tooltip and legend content
// at render time, but (as of recharts v3) does not expose them on the public
// prop types, and the payload items are loosely typed. Model the fields we use
// explicitly so the component is type-safe.
type ChartPayloadItem = {
name?: string | number;
value?: number | string;
@@ -356,15 +360,13 @@ const getPayloadConfigFromPayload = (
key in payload &&
typeof payload[key as keyof typeof payload] === 'string'
) {
configLabelKey = payload[key as keyof typeof payload] as string;
configLabelKey = payload[key as keyof typeof payload];
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
configLabelKey = payloadPayload[key as keyof typeof payloadPayload];
}
return configLabelKey in config ? config[configLabelKey] : config[key];
+14 -16
View File
@@ -1,21 +1,19 @@
import * as React from 'react';
const MOBILE_BREAKPOINT = 768;
const MOBILE_QUERY = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
export const useIsMobile = () => {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener('change', onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener('change', onChange);
}, []);
return !!isMobile;
const subscribe = (onChange: () => void) => {
const mql = window.matchMedia(MOBILE_QUERY);
mql.addEventListener('change', onChange);
return () => mql.removeEventListener('change', onChange);
};
// True below the mobile breakpoint. Server renders and hydration assume
// desktop, then the browser's media query takes over.
export const useIsMobile = () =>
React.useSyncExternalStore(
subscribe,
() => window.matchMedia(MOBILE_QUERY).matches,
() => false,
);
+2 -8
View File
@@ -294,10 +294,7 @@ export const ImageCropApply = ({
if (asChild) {
return (
<Slot.Root
onClick={handleClick}
{...(props as ComponentProps<typeof Slot.Root>)}
>
<Slot.Root onClick={handleClick} {...props}>
{children}
</Slot.Root>
);
@@ -329,10 +326,7 @@ export const ImageCropReset = ({
if (asChild) {
return (
<Slot.Root
onClick={handleClick}
{...(props as ComponentProps<typeof Slot.Root>)}
>
<Slot.Root onClick={handleClick} {...props}>
{children}
</Slot.Root>
);
+5 -17
View File
@@ -1,20 +1,8 @@
import { cx } from 'class-variance-authority';
import { twMerge } from 'tailwind-merge';
export const cn = (...inputs: Parameters<typeof cx>) => twMerge(cx(inputs));
export const ccn = ({
context,
className,
on = '',
off = '',
}: {
context: boolean;
className: string;
on: string;
off: string;
}) => twMerge(className, context ? on : off);
// Re-exports only. The Next app lists @gib/ui in `optimizePackageImports`,
// which resolves each named import straight to its own module so a page that
// uses Button never loads Chart (and Recharts). That only works while nothing
// is declared in this file.
export { cn, ccn } from './utils';
export {
Accordion,
AccordionItem,
+16
View File
@@ -0,0 +1,16 @@
import { cx } from 'class-variance-authority';
import { twMerge } from 'tailwind-merge';
export const cn = (...inputs: Parameters<typeof cx>) => twMerge(cx(inputs));
export const ccn = ({
context,
className,
on = '',
off = '',
}: {
context: boolean;
className: string;
on: string;
off: string;
}) => twMerge(className, context ? on : off);
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env sh
set -eu
ROOT_DIR="$(cd -- "$(dirname -- "$0")/.." && pwd)"
if [ -n "${SENTRY_AUTH_TOKEN:-}" ] &&
[ -n "${NEXT_PUBLIC_SENTRY_URL:-}" ] &&
! curl \
--connect-timeout 3 \
--max-time 10 \
--silent \
--show-error \
--output /dev/null \
"${NEXT_PUBLIC_SENTRY_URL%/}/api/0/"; then
echo "Sentry is unreachable; continuing without source-map upload." >&2
SENTRY_AUTH_TOKEN=""
SKIP_ENV_VALIDATION=true
NEXT_PUBLIC_SENTRY_URL="http://127.0.0.1:9"
export NEXT_PUBLIC_SENTRY_URL SENTRY_AUTH_TOKEN SKIP_ENV_VALIDATION
fi
cd "$ROOT_DIR/apps/next"
exec bun run build:docker
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env node
// ci-export-env.mjs [environment]
//
// CI's counterpart to scripts/export-env: prints an Infisical environment
// (default `staging`) as dotenv on stdout, in the same quoting export-env uses.
// It logs in as the `convexmonorepo-ci` machine identity through Universal Auth, using
// INFISICAL_CLIENT_ID and INFISICAL_CLIENT_SECRET from the job's Gitea secrets,
// and reads the project and host from .infisical.json. Plain Node, no CLI to
// install, so it runs in every job whether or not Bun is set up.
import { readFile } from 'node:fs/promises';
const environment = process.argv[2] ?? 'staging';
const { INFISICAL_CLIENT_ID: clientId, INFISICAL_CLIENT_SECRET: clientSecret } =
process.env;
const fail = (message) => {
console.error(`ci-export-env: ${message}`);
process.exit(1);
};
if (!clientId || !clientSecret)
fail('INFISICAL_CLIENT_ID and INFISICAL_CLIENT_SECRET must be set.');
const { workspaceId, domain } = JSON.parse(
await readFile(new URL('../.infisical.json', import.meta.url), 'utf8'),
);
const request = async (path, init) => {
const response = await fetch(`${domain}${path}`, init);
if (!response.ok)
fail(`${init?.method ?? 'GET'} ${path} returned ${response.status}.`);
return response.json();
};
const { accessToken } = await request('/v1/auth/universal-auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clientId, clientSecret }),
});
const query = new URLSearchParams({
workspaceId,
environment,
secretPath: '/',
expandSecretReferences: 'true',
include_imports: 'true',
});
const { secrets, imports = [] } = await request(`/v3/secrets/raw?${query}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
// Imported secrets first so the environment's own values win.
const values = new Map();
for (const { secretKey, secretValue } of [
...imports.flatMap((folder) => folder.secrets),
...secrets,
])
values.set(secretKey, secretValue);
const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
for (const [key, value] of [...values].sort(([a], [b]) => a.localeCompare(b)))
process.stdout.write(`${key}=${quote(value)}\n`);
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Install on a runner, and survive a half-written cache entry.
#
# Runners have failed twice with "Fail extracting tarball" on a different
# package each time, on different machines, and passed on a re-run. The cache
# is shared between jobs on a host, so a job that dies mid-write leaves an
# entry the next job cannot read. Clearing it and installing again costs a
# couple of minutes; a human re-running the job costs however long it takes
# someone to notice.
set -euo pipefail
if bun install --frozen-lockfile; then exit 0; fi
echo "Install failed. Clearing the bun cache and trying once more." >&2
rm -rf "${BUN_INSTALL_CACHE_DIR:-$HOME/.bun/install/cache}"
bun install --frozen-lockfile
+8 -3
View File
@@ -51,9 +51,9 @@ for i in $(seq 1 30); do
sleep 1
done
info "Waiting for Convex at http://localhost:${BACKEND_PORT:-3210}"
info "Waiting for Convex at http://localhost:${BACKEND_PORT:-3240}"
for i in $(seq 1 60); do
curl -fs "http://localhost:${BACKEND_PORT:-3210}/version" >/dev/null 2>&1 && break
curl -fs "http://localhost:${BACKEND_PORT:-3240}/version" >/dev/null 2>&1 && break
[ "$i" -lt 60 ] || die "Convex did not become ready."
sleep 2
done
@@ -69,6 +69,11 @@ fi
info "Deploying Convex schema and functions"
(cd "$ROOT_DIR/packages/backend" && bun run setup)
# Env sync runs after the deploy: changing deployment env re-analyzes the
# deployed modules, which fails on a fresh volume before the first deploy.
info "Synchronizing the Convex function runtime environment"
sh "$ROOT_DIR/scripts/with-env" dev -- bash "$ROOT_DIR/scripts/sync-local-convex-env"
convex_env_names="$(
sh "$ROOT_DIR/scripts/with-env" dev -- bash -c \
'cd packages/backend && bunx convex env list' 2>/dev/null \
@@ -92,4 +97,4 @@ fi
info "Seeding local Payload from snapshot if needed"
bash "$ROOT_DIR/scripts/db/seed-payload" --yes
printf '\nLocal stack ready:\n App: http://localhost:3000\n Convex: http://localhost:%s\n Dashboard: http://localhost:%s\n Payload Postgres: localhost:%s\n' "${BACKEND_PORT:-3210}" "${DASHBOARD_PORT:-6791}" "${POSTGRES_PORT:-5432}"
printf '\nLocal stack ready:\n App: http://localhost:3000\n Convex: http://localhost:%s\n Dashboard: http://localhost:%s\n Payload Postgres: localhost:%s\n' "${BACKEND_PORT:-3240}" "${DASHBOARD_PORT:-6794}" "${POSTGRES_PORT:-5436}"
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
#
# Kill any lingering host-level dev servers (Next.js, Expo Metro, Convex CLI
# watchers, turbo orchestrators) that survived a Ctrl-C. Does not touch the
# dockerized Convex/Postgres stack — use `bun db:down` for that.
#
# Usage: scripts/dev-kill (wired as `bun dev:kill`)
set -euo pipefail
info() { printf '\033[36m▶ %s\033[0m\n' "$*"; }
ok() { printf '\033[32m✓ %s\033[0m\n' "$*"; }
PORTS=(3000 3001 8081 19000 19001 19002)
PATTERNS=('next-server' 'next dev' 'expo start' 'turbo run dev' 'convex dev')
killed=false
for port in "${PORTS[@]}"; do
pids="$(lsof -ti "tcp:$port" -sTCP:LISTEN 2>/dev/null || true)"
[ -z "$pids" ] && continue
for pid in $pids; do
cmd="$(ps -p "$pid" -o comm= 2>/dev/null || true)"
info "Killing $cmd (pid $pid) on port $port"
kill "$pid" 2>/dev/null || true
killed=true
done
done
for pattern in "${PATTERNS[@]}"; do
pids="$(pgrep -f "$pattern" 2>/dev/null || true)"
[ -z "$pids" ] && continue
for pid in $pids; do
info "Killing '$pattern' (pid $pid)"
kill "$pid" 2>/dev/null || true
killed=true
done
done
if [ "$killed" = true ]; then
sleep 1
ok "Dev servers stopped."
else
ok "No dev servers were running."
fi
+6
View File
@@ -16,6 +16,12 @@ fi
[ -f "$ROOT_DIR/.infisical.json" ] || { echo "export-env: run 'infisical init' in this repository." >&2; exit 1; }
command -v infisical >/dev/null 2>&1 || { echo "export-env: Infisical CLI is required." >&2; exit 1; }
if [ -f "$ROOT_DIR/scripts/infisical-profile" ]; then
# shellcheck source=/dev/null
. "$ROOT_DIR/scripts/infisical-profile"
ensure_infisical_profile "$ROOT_DIR/scripts/flags/infisical.env"
fi
(cd "$ROOT_DIR" && infisical export --env="$ENVIRONMENT" --format=dotenv --silent) || {
echo "export-env: failed to export '$ENVIRONMENT'; check login and project access." >&2
exit 1
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env sh
#
# ensure_infisical_profile <profile_env_file>
#
# Infisical's CLI keeps a single active login (~/.infisical/infisical-config.json)
# shared across every project on the machine. If you juggle multiple Infisical
# accounts (e.g. a client org vs. a personal org), running with-env in one repo
# can silently pull secrets while logged in as the wrong user.
#
# This switches the active `loggedInUserEmail` to the email configured in
# scripts/flags/infisical.env (INFISICAL_EMAIL=...) before infisical runs,
# provided that email is already logged in locally (`infisical login`).
# It is a no-op if the flag file is absent, only one user is logged in, or the
# expected user isn't logged in yet.
#
# POSIX sh, no bashisms: matches the rest of scripts/ which must stay
# busybox-compatible.
warn_infisical_profile() {
echo "Warning: infisical-profile: $*" >&2
}
ensure_infisical_profile() {
profile_env_file="${1:-}"
if [ -z "$profile_env_file" ] || [ ! -f "$profile_env_file" ]; then
return 0
fi
set -a
# shellcheck source=/dev/null
if ! . "$profile_env_file"; then
set +a
warn_infisical_profile "could not read $profile_env_file. Continuing with current Infisical profile."
return 0
fi
set +a
expected_email="${INFISICAL_EMAIL:-}"
if [ -z "$expected_email" ]; then
return 0
fi
config="${INFISICAL_CONFIG_PATH:-$HOME/.infisical/infisical-config.json}"
lock="${INFISICAL_LOCK_PATH:-$HOME/.infisical/.switch.lock}"
if [ ! -f "$config" ]; then
warn_infisical_profile "Infisical config not found at $config. Continuing with current Infisical profile."
return 0
fi
if ! command -v jq >/dev/null 2>&1; then
warn_infisical_profile "jq is required to select Infisical profile. Continuing with current Infisical profile."
return 0
fi
if ! command -v flock >/dev/null 2>&1; then
warn_infisical_profile "flock is required to safely select Infisical profile. Continuing with current Infisical profile."
return 0
fi
if ! mkdir -p "$(dirname "$lock")"; then
warn_infisical_profile "could not create Infisical lock directory. Continuing with current Infisical profile."
return 0
fi
(
if ! flock 9; then
warn_infisical_profile "could not lock Infisical profile config. Continuing with current Infisical profile."
return 0
fi
user_count="$(jq -r '(.loggedInUsers // []) | length' "$config" 2>/dev/null)" || {
warn_infisical_profile "could not parse Infisical config. Continuing with current Infisical profile."
return 0
}
if [ "$user_count" -lt 2 ]; then
return 0
fi
current_email="$(jq -r '.loggedInUserEmail // ""' "$config" 2>/dev/null)" || {
warn_infisical_profile "could not read current Infisical profile. Continuing with current Infisical profile."
return 0
}
if [ "$current_email" = "$expected_email" ]; then
return 0
fi
domain="$(
jq -er --arg email "$expected_email" \
'.loggedInUsers[] | select(.email == $email) | .domain' \
"$config" 2>/dev/null
)" || {
warn_infisical_profile "Infisical profile $expected_email is not logged in locally. Continuing with current Infisical profile."
return 0
}
tmp="$(mktemp)" || {
warn_infisical_profile "could not create temp file for Infisical profile switch. Continuing with current Infisical profile."
return 0
}
if jq --arg email "$expected_email" --arg domain "$domain" \
'.loggedInUserEmail = $email | .LoggedInUserDomain = $domain' \
"$config" > "$tmp"; then
chmod 600 "$tmp"
mv "$tmp" "$config"
else
rm -f "$tmp"
warn_infisical_profile "failed to update Infisical profile. Continuing with current Infisical profile."
fi
) 9>"$lock"
return 0
}
+9
View File
@@ -7,6 +7,15 @@ const __dirname = path.dirname(__filename);
const usesendDir = path.join(__dirname, '..', 'node_modules', 'usesend-js');
const ensureReplacement = (content, searchValue, replaceValue, filePath) => {
// Removal step (empty replacement): it's already applied once the search
// snippet is gone. Guard on searchValue, not replaceValue — every string
// "includes" the empty string, which would make this a silent no-op.
if (replaceValue === '') {
return content.includes(searchValue)
? content.replace(searchValue, replaceValue)
: content;
}
if (content.includes(replaceValue)) {
return content;
}
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
VALUE_FILE=""
readonly RUNTIME_ENV_NAMES=(
AUTH_AUTHENTIK_ID
AUTH_AUTHENTIK_SECRET
AUTH_AUTHENTIK_ISSUER
USESEND_API_KEY
USESEND_URL
USESEND_FROM_EMAIL
)
readonly DEPLOYMENT_ENV_NAMES=(
CONVEX_SELF_HOSTED_URL
CONVEX_SELF_HOSTED_ADMIN_KEY
)
die() { printf 'sync-local-convex-env: %s\n' "$*" >&2; exit 1; }
cleanup() {
if [ -n "$VALUE_FILE" ] && [ -f "$VALUE_FILE" ]; then
rm -f "$VALUE_FILE"
fi
}
trap cleanup EXIT INT TERM HUP
for name in "${DEPLOYMENT_ENV_NAMES[@]}" "${RUNTIME_ENV_NAMES[@]}"; do
[ -n "${!name:-}" ] || die "Missing required environment variable: $name"
done
VALUE_FILE="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-convex-env.value.XXXXXX")"
chmod 600 "$VALUE_FILE"
for name in "${RUNTIME_ENV_NAMES[@]}"; do
printf '%s' "${!name}" > "$VALUE_FILE"
(
cd "$ROOT_DIR/packages/backend"
env -u CONVEX_DEPLOY_KEY -u CONVEX_DEPLOYMENT \
CONVEX_SELF_HOSTED_URL="$CONVEX_SELF_HOSTED_URL" \
CONVEX_SELF_HOSTED_ADMIN_KEY="$CONVEX_SELF_HOSTED_ADMIN_KEY" \
bunx convex env set "$name" --from-file "$VALUE_FILE" >/dev/null
)
done
printf 'Local Convex runtime environment synchronized.\n'
+6 -6
View File
@@ -13,16 +13,16 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@eslint/compat": "^2.0.3",
"@eslint/compat": "^2.1.1",
"@eslint/js": "catalog:",
"@next/eslint-plugin-next": "^16.2.1",
"@next/eslint-plugin-next": "^16.3.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prefer-arrow-functions": "^3.9.1",
"eslint-plugin-prefer-arrow-functions": "^3.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-turbo": "^2.8.20",
"typescript-eslint": "^8.57.2"
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-turbo": "^2.11.6",
"typescript-eslint": "^8.71.0"
},
"devDependencies": {
"@gib/prettier-config": "workspace:*",
+1 -1
View File
@@ -13,7 +13,7 @@
"dependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
"prettier": "catalog:",
"prettier-plugin-tailwindcss": "^0.7.2"
"prettier-plugin-tailwindcss": "^0.7.4"
},
"devDependencies": {
"@gib/tsconfig": "workspace:*",
+1 -1
View File
@@ -15,7 +15,7 @@
},
"dependencies": {
"@tailwindcss/postcss": "catalog:",
"postcss": "^8.5.8",
"postcss": "^8.5.28",
"tailwindcss": "catalog:"
},
"devDependencies": {
+5 -2
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://v2-9-18.turborepo.dev/schema.json",
"$schema": "https://v2-11-6.turborepo.dev/schema.json",
"globalDependencies": [".infisical.json", ".local/*.env"],
"globalEnv": [
"NODE_ENV",
@@ -10,7 +10,9 @@
"PAYLOAD_DB_URL",
"NEXT_PUBLIC_SITE_URL",
"NEXT_PUBLIC_CONVEX_URL",
"NEXT_PUBLIC_PLAUSIBLE_URL",
"NEXT_PUBLIC_PLAUSIBLE_SRC",
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_SENTRY_DSN",
"NEXT_PUBLIC_SENTRY_URL",
"NEXT_PUBLIC_SENTRY_ORG",
@@ -53,6 +55,7 @@
],
"globalPassThroughEnv": ["NODE_ENV"],
"ui": "tui",
"agentGuidance": false,
"tasks": {
"topo": {
"dependsOn": ["^topo"]