Template
Compare commits
3
Commits
a4d4355886
...
7d1884be41
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d1884be41 | ||
|
|
f39376a887 | ||
|
|
1af6b43b1a |
No files matched your search
@@ -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"
|
||||
@@ -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
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"workspaceId": "b2760150-c581-4e0d-acb5-b4da51cee58d",
|
||||
"defaultEnvironment": "",
|
||||
"gitBranchToEnvironmentMapping": null
|
||||
"gitBranchToEnvironmentMapping": null,
|
||||
"domain": "https://infisical.gbrown.org/api"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"jsx": "react-native",
|
||||
"checkJs": false,
|
||||
"types": ["node"],
|
||||
"moduleSuffixes": [".ios", ".android", ".native", ""],
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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:",
|
||||
|
||||
@@ -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?:
|
||||
|
||||
@@ -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;
|
||||
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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());
|
||||
};
|
||||
@@ -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";`);
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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 */
|
||||
@@ -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,
|
||||
});
|
||||
+9
-5
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
Executable
+23
@@ -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
|
||||
Executable
+62
@@ -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`);
|
||||
Executable
+14
@@ -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
@@ -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}"
|
||||
Executable
+44
@@ -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
|
||||
@@ -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
|
||||
|
||||
Executable
+117
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Executable
+47
@@ -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'
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/postcss": "catalog:",
|
||||
"postcss": "^8.5.8",
|
||||
"postcss": "^8.5.28",
|
||||
"tailwindcss": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+5
-2
@@ -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"]
|
||||
|
||||
Reference in new issue
Block a user