Move to infisical. Create local dev environment. Add ci gates. Modernize repo
Build and Push Next App / quality (push) Successful in 1m8s
Build and Push Next App / build-next (push) Successful in 2m59s

This commit is contained in:
Gabriel Brown
2026-06-21 14:04:02 -05:00
parent 86e2fdc82e
commit a12bf6071b
79 changed files with 1612 additions and 42168 deletions
+4 -3
View File
@@ -18,9 +18,10 @@ out
.git .git
.gitignore .gitignore
*.log *.log
#.env .env
#.env.* .env.*
!.env.example .local
**/.local
.vscode .vscode
.idea .idea
-64
View File
@@ -1,64 +0,0 @@
# Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
# Keep this file up-to-date when you add new variables to \`.env\`.
# This file will be committed to version control, so make sure not to have any secrets in it.
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
## Next.js ##
NODE_ENV=
SENTRY_AUTH_TOKEN=
PAYLOAD_SECRET= # openssl rand -hex 32 | wl-copy
PAYLOAD_DB_URL= # postgresql://user:password@host:5432/db_name
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_CONVEX_URL=https://api.convex.example.com # convex-backend:3210
NEXT_PUBLIC_PLAUSIBLE_URL=https://plausible.example.com
NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_SENTRY_URL=https://sentry.example.com
NEXT_PUBLIC_SENTRY_ORG=sentry
NEXT_PUBLIC_SENTRY_PROJECT_NAME=example
## Convex ##
CONVEX_SELF_HOSTED_URL=https://api.convex.example.com # convex-backend:3210
CONVEX_SELF_HOSTED_ADMIN_KEY= # Generate after hosted on docker
# Convex Auth
CONVEX_SITE_URL=http://localhost:3000 # Always localhost:3000 for local dev; update in Convex Dashboard for production
USESEND_API_KEY=
USESEND_URL=https://usesend.example.com
USESEND_FROM_EMAIL=Convex Admin <admin@convexmonorepo.gbrown.org>
AUTH_AUTHENTIK_ID=
AUTH_AUTHENTIK_SECRET=
AUTH_AUTHENTIK_ISSUER=
## Docker Compose Variables for Next App ##
NETWORK=nginx-bridge
NEXT_CONTAINER_NAME=convexmonorepo
NEXT_DOMAIN=convexmonorepo.gbrown.org
#NEXT_PORT=
## Docker Compose Variables for Self hosted Convex ##
BACKEND_TAG=latest
DASHBOARD_TAG=latest
BACKEND_CONTAINER_NAME=convex-backend
DASHBOARD_CONTAINER_NAME=convex-dashboard
BACKEND_DOMAIN=convex.convexmonorepo.gbrown.org
DASHBOARD_DOMAIN=dashboard.convexmonorepo.gbrown.org
INSTANCE_NAME=convex
#INSTANCE_SECRET=
CONVEX_CLOUD_ORIGIN=https://api.convexmonorepo.gbrown.org
CONVEX_SITE_ORIGIN=https://convex.convexmonorepo.gbrown.org
NEXT_PUBLIC_DEPLOYMENT_URL=https://api.convexmonorepo.gbrown.org
DISABLE_BEACON=true
REDACT_LOGS_TO_CLIENT=true
DO_NOT_REQUIRE_SSL=true
POSTGRES_URL= #postgresql://user:password@host:5432/db_name
#BACKEND_PORT=
#DASHBOARD_PORT
#SITE_PROXY_PORT=
#ACTIONS_USER_TIMEOUT_SECS=
#RUST_LOG=
#RUST_BACKTRACE=
## Docker Compose Variables for Postgres ##
POSTGRES_CONTAINER_NAME=convexmonorepo-postgres
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=convexmonorepo_payload
+35 -29
View File
@@ -2,49 +2,55 @@ name: Build and Push Next App
on: on:
push: push:
branches: branches: [main]
- main
paths: paths:
- 'apps/next/**' - 'apps/**'
- 'packages/ui/**' - 'packages/**'
- 'scripts/build-next-app'
- 'tools/**' - 'tools/**'
- 'scripts/**'
- 'docker/**' - 'docker/**'
- '.gitea/workflows/build-next.yml' - '.gitea/workflows/build-next.yml'
- '.infisical.json'
- 'package.json'
- 'bun.lock'
- 'turbo.json'
jobs: jobs:
build-next: quality:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - uses: actions/checkout@v4
uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2
with:
- name: Create .env file bun-version: 1.3.10
- run: bun install --frozen-lockfile
- name: Lint, typecheck, and test
env:
DOTENV_PROD: ${{ secrets.DOTENV_PROD }}
run: | run: |
cat > .env <<'EOF' env_file="$(mktemp)"
${{ secrets.DOTENV_PROD }} trap 'rm -f "$env_file"' EXIT
EOF printf '%s\n' "$DOTENV_PROD" > "$env_file"
bunx dotenv -e "$env_file" -- env NODE_ENV=test SKIP_E2E=1 bun run ci:check
build-next:
needs: [quality]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to container registry - 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
env:
DOTENV_PROD: ${{ secrets.DOTENV_PROD }}
run: | run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.gbrown.org -u "${{ secrets.REGISTRY_USER }}" --password-stdin env_file="$(mktemp)"
trap 'rm -f "$env_file"' EXIT
- name: Build app using project script printf '%s\n' "$DOTENV_PROD" > "$env_file"
run: | CI_ENV_FILE="$env_file" ./scripts/build-next-app staging
chmod +x ./scripts/build-next-app - name: Tag and push image
./scripts/build-next-app
- name: Show built images
run: |
docker images
- name: Tag built image
run: | 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:${{ gitea.sha }}
docker tag convexmonorepo-next:latest git.gbrown.org/gib/convexmonorepo-next:latest 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:${{ gitea.sha }}
docker push git.gbrown.org/gib/convexmonorepo-next:latest docker push git.gbrown.org/gib/convexmonorepo-next:latest
+13 -1
View File
@@ -10,6 +10,10 @@ node_modules
# testing # testing
coverage coverage
**/.cache/
# generated Convex client/server bindings (tracked, but not linted/formatted)
packages/backend/convex/_generated/
# next.js # next.js
.next/ .next/
@@ -48,9 +52,17 @@ yarn-error.log*
.env.test.local .env.test.local
.env.production.local .env.production.local
.env .env
.env.staging
.local/
# turbo # turbo
.turbo .turbo/
**/.turbo/
# playwright e2e
apps/next/tests/e2e/.auth/
apps/next/test-results/
apps/next/playwright-report/
# tanstack # tanstack
.tanstack .tanstack
+1
View File
@@ -0,0 +1 @@
bunx lint-staged --concurrent 1
+1
View File
@@ -0,0 +1 @@
bun run ci:check
+5
View File
@@ -0,0 +1,5 @@
{
"workspaceId": "b2760150-c581-4e0d-acb5-b4da51cee58d",
"defaultEnvironment": "",
"gitBranchToEnvironmentMapping": null
}
+60 -1589
View File
File diff suppressed because it is too large Load Diff
+74 -477
View File
@@ -1,495 +1,92 @@
# Convex Turbo Monorepo # Convex Turbo Monorepo
A production-ready Turborepo starter with Next.js, Expo, self-hosted Convex, and A reusable Bun/Turborepo template with Next.js 16, Expo, self-hosted Convex,
embedded Payload CMS for live-editable marketing content. Built with TypeScript, Payload CMS, shared UI/config packages, Vitest, and Docker deployment.
Tailwind CSS, and modern tooling.
--- ## Local setup
## What's Inside? Requirements: Bun 1.3.10, Docker or Podman, Node 22, and the Infisical CLI.
### Apps & Packages ```sh
bun install --frozen-lockfile
- **`apps/next`** - Next.js 16 web application with App Router infisical login
- **`apps/expo`** - Expo 54 mobile application _(in progress)_ infisical init
- **`@gib/backend`** - Self-hosted Convex backend with authentication bun db:up
- **`@gib/ui`** - Shared shadcn/ui component library bun dev:next
- **`@gib/eslint-config`** - ESLint configuration
- **`@gib/prettier-config`** - Prettier configuration with import sorting
- **`@gib/tailwind-config`** - Tailwind CSS v4 configuration
- **`@gib/tsconfig`** - Shared TypeScript configurations
### Tech Stack
- **Framework:** Next.js 16 (App Router) + Expo 54
- **Backend:** Convex (self-hosted)
- **Auth:** @convex-dev/auth with Authentik OAuth & Password providers
- **CMS:** Payload CMS embedded inside the Next.js app
- **Styling:** Tailwind CSS v4 + shadcn/ui
- **Language:** TypeScript (strict mode)
- **Package Manager:** Bun
- **Monorepo:** Turborepo
- **Deployment:** Docker (standalone)
---
## Getting Started
This is a self-hosted template. The full setup requires a server (home server or VPS)
to host the Convex backend and dashboard, and a reverse proxy (nginx-proxy-manager is
recommended) to expose them over HTTPS. The Next.js app can run locally in dev mode
once the Convex containers are reachable.
### Prerequisites
- [Bun](https://bun.sh) (v1.2+)
- [Docker](https://www.docker.com/) & Docker Compose (for self-hosted Convex)
- Node.js 22+ (for compatibility)
- A running nginx-proxy-manager instance (or similar reverse proxy) to expose Convex over HTTPS
---
### Step 1 — Clone & Install
```bash
git clone https://git.gbrown.org/gib/convex-monorepo
cd convex-monorepo
bun install
``` ```
If you're using this as a template for a new project, remove the existing remote and The committed `.infisical.json` links this repository to its own Infisical
add your own: project. Local commands read `dev` by default and never fall back to `.env`
files. Select staging with `INFISICAL_ENV=staging bun dev:next`.
```bash Local services:
git remote remove origin
git remote add origin https://your-git-host.com/your/new-repo.git - Next.js: `http://localhost:3000`
- Convex API: `http://localhost:3210`
- Convex dashboard: `http://localhost:6791`
- Payload Postgres: `localhost:5432`
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.
The commented `POSTGRES_URL` in Compose is an opt-in example for cloned projects.
```sh
bun db:down # stop; preserve Payload and Convex data
bun db:down:wipe # remove both volumes, generated admin key, and seed marker
bun db:sync:payload # refresh/apply the local Payload seed snapshot from staging
``` ```
--- `db:sync:payload` reads the staging `PAYLOAD_DB_URL`, saves a private PostgreSQL
snapshot under `.local/`, and replaces only the localhost Payload database. It
never writes to staging or Convex. Normal `bun db:up` never contacts staging;
it only restores `.local/payload-staging.dump` when the local database marker is
missing or stale. The marker is `.local/payload-seed-state.env`, not a database
table, so Payload schema push will not try to drop it. Stop the Next development
server first so it does not reconnect while the database is being replaced. The
snapshot includes Payload users and password hashes, allowing the same admin
credentials locally; protect it as production-derived data and delete it when no
longer needed.
### Step 2 — Configure the Single Root Environment File Physical devices cannot resolve their own `localhost`; override the public
Convex URL with the development host's LAN address when testing Expo on-device.
```bash ## Environment model
cp .env.example .env
- Local `dev` and `staging`: Infisical.
- Generated local state: `.local/<environment>.generated.env`.
- CI/CD: Gitea `DOTENV_PROD`, materialized only as a temporary runner file.
- Docker compilation: explicit Compose build args; `.env*` stays outside the
image context.
Run `sh scripts/with-env dev -- <command>` for an environment-aware command or
`sh scripts/export-env dev` to materialize a temporary merged dotenv stream.
Do not commit or maintain root `.env` files.
## Development and quality
```sh
bun dev:next
bun dev:expo
bun lint:ws
bun format
bun lint
bun typecheck
bun test:unit
bun test:integration
bun test:component
SKIP_E2E=1 bun run ci:check
``` ```
The root `/.env` is the single required env file for this repo. It is used for: `bun test:e2e` starts the isolated local stack and currently performs generic
stack smoke checks. It skips in CI and when `SKIP_E2E=1` is set.
- app/package runtime env vars Shared dependency versions belong in root catalogs. Edit the root catalog, run
- Next.js build-time env vars `bun install`, then `bun lint:ws`; do not run `bun update` inside a workspace.
- Payload env vars
- Docker Compose interpolation
- helper scripts in `scripts/`
Fill it in with your values. The Docker-related variables in the same file control the
Convex containers and Next.js container — you'll need to choose:
- `INSTANCE_NAME` — a unique name for your Convex instance
- `INSTANCE_SECRET` — a secret string (generate something random)
- `CONVEX_CLOUD_ORIGIN` — the public HTTPS URL for your Convex backend API (e.g. `https://api.convex.example.com`)
- `CONVEX_SITE_ORIGIN` — the public HTTPS URL for Convex Auth HTTP routes (e.g. `https://api.convex.example.com`)
- `NEXT_PUBLIC_DEPLOYMENT_URL` — the URL for the Convex dashboard (e.g. `https://dashboard.convex.example.com`)
- `NEXT_CONTAINER_NAME`, `BACKEND_CONTAINER_NAME`, and `DASHBOARD_CONTAINER_NAME` — the Docker service container names
Do not create or rely on a separate `docker/.env`.
---
### Step 3 — Start the Convex Containers
```bash
./scripts/docker-compose up -d backend dashboard
```
Wait a moment for `convex-backend` to pass its health check, then verify both
containers are running:
```bash
./scripts/docker-compose ps
```
Reverse-proxy the two Convex services through nginx-proxy-manager (or your preferred
proxy) to the URLs you chose in Step 2. Both must be reachable over HTTPS before you
can proceed.
---
### Step 4 — Generate the Convex Admin Key
With the backend container running, generate the admin key:
```bash
./scripts/generate-convex-admin-key
```
Copy the printed key — you'll need it as `CONVEX_SELF_HOSTED_ADMIN_KEY` in the root
`.env` file.
---
### Step 5 — Finish Configuring Root Environment Variables
Fill out all values in root `/.env`:
```bash
# Next.js
NODE_ENV=development
SENTRY_AUTH_TOKEN= # From your self-hosted Sentry
PAYLOAD_SECRET= # openssl rand -hex 32
PAYLOAD_DB_URL=postgresql://user:password@host:5432/db_name
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_CONVEX_URL=https://api.convex.example.com
NEXT_PUBLIC_PLAUSIBLE_URL=https://plausible.example.com
NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_SENTRY_URL=https://sentry.example.com
NEXT_PUBLIC_SENTRY_ORG=sentry
NEXT_PUBLIC_SENTRY_PROJECT_NAME=my-project
# Convex
CONVEX_SELF_HOSTED_URL=https://api.convex.example.com
CONVEX_SELF_HOSTED_ADMIN_KEY= # From Step 4
CONVEX_SITE_URL=http://localhost:3000 # Always localhost:3000 for local dev
# Auth (synced to Convex in Step 6)
USESEND_API_KEY=
USESEND_URL=https://usesend.example.com
USESEND_FROM_EMAIL=My App <noreply@example.com>
AUTH_AUTHENTIK_ID=
AUTH_AUTHENTIK_SECRET=
AUTH_AUTHENTIK_ISSUER=https://auth.example.com/application/o/my-app/
# Docker Compose
NETWORK=nginx-bridge
NEXT_CONTAINER_NAME=next-app
BACKEND_CONTAINER_NAME=convex-backend
DASHBOARD_CONTAINER_NAME=convex-dashboard
INSTANCE_NAME=convex
INSTANCE_SECRET=
CONVEX_CLOUD_ORIGIN=https://api.convex.example.com
CONVEX_SITE_ORIGIN=https://convex.convex.example.com
NEXT_PUBLIC_DEPLOYMENT_URL=https://dashboard.convex.example.com
POSTGRES_URL=postgresql://user:password@host:5432/convex
```
---
### Step 6 — Generate JWT Keys & Sync Environment Variables to Convex
Generate the RS256 JWT keypair needed for Convex Auth:
```bash
cd packages/backend
bun run scripts/generateKeys.mjs
```
This prints `JWT_PRIVATE_KEY` and `JWKS` values. Sync them to your Convex deployment
along with all other backend environment variables:
```bash
# From packages/backend/
bun with-env npx convex env set JWT_PRIVATE_KEY "your-private-key"
bun with-env npx convex env set JWKS "your-jwks"
bun with-env npx convex env set AUTH_AUTHENTIK_ID "your-client-id"
bun with-env npx convex env set AUTH_AUTHENTIK_SECRET "your-client-secret"
bun with-env npx convex env set AUTH_AUTHENTIK_ISSUER "your-issuer-url"
bun with-env npx convex env set USESEND_API_KEY "your-api-key"
bun with-env npx convex env set USESEND_URL "https://usesend.example.com"
bun with-env npx convex env set USESEND_FROM_EMAIL "My App <noreply@example.com>"
```
**For production auth to work**, you must also update `CONVEX_SITE_URL` in the Convex
Dashboard to your production Next.js URL. Go to
`https://dashboard.convex.example.com` → Settings → Environment Variables and set:
```
CONVEX_SITE_URL = https://example.com
```
The root `.env` value of `http://localhost:3000` is correct for local dev and should
not be changed — only update it in the Dashboard for production.
---
### Step 7 — Start the Development Server
```bash
# From project root
bun dev:next # Next.js app + Convex backend (most common)
# or
bun dev # All apps (Next.js + Expo + Backend)
```
**App URLs:**
- **Next.js:** http://localhost:3000
- **Convex Dashboard:** https://dashboard.convex.example.com
### Docker Helper Scripts
This repo includes helper scripts so you do not have to keep passing `--env-file` and
`-f` manually to Docker Compose.
Useful commands:
```bash
./scripts/docker-compose ps
./scripts/docker-compose up -d backend dashboard
./scripts/docker-compose up -d next
./scripts/build-next-app
./scripts/update-next-app
./scripts/update-convex
./scripts/generate-convex-admin-key
```
`./scripts/docker-compose` also accepts short service aliases like `next`, `backend`,
and `dashboard` and maps them to the container names from root `/.env`.
---
## Development Commands
```bash
# Development
bun dev # Run all apps
bun dev:next # Next.js + Convex backend
bun dev:expo # Expo + Convex backend
bun dev:backend # Convex backend only
# Quality Checks
bun typecheck # Type checking (recommended for testing)
bun lint # Lint all packages
bun lint:fix # Lint and auto-fix
bun format # Check formatting
bun format:fix # Fix formatting
# Build
bun build # Build all packages (production)
# Utilities
bun ui-add # Add shadcn/ui components
bun clean # Clean node_modules
bun clean:ws # Clean workspace caches
```
### Single Package Commands
Use Turborepo filters to target specific packages:
```bash
bun turbo run dev -F @gib/next # Run Next.js only
bun turbo run typecheck -F @gib/backend # Typecheck backend
bun turbo run lint -F @gib/ui # Lint UI package
```
---
## Project Structure
```
convex-monorepo/
├── apps/
│ ├── next/ # Next.js web app
│ │ ├── src/
│ │ │ ├── app/ # App Router pages
│ │ │ ├── components/ # React components
│ │ │ └── lib/ # Utilities
│ │ └── package.json
│ └── expo/ # Expo mobile app (WIP)
├── packages/
│ ├── backend/ # Convex backend
│ │ ├── convex/ # Convex functions (synced to deployment)
│ │ ├── scripts/ # Utilities (generateKeys.mjs)
│ │ └── types/ # Shared types
│ └── ui/ # shadcn/ui components
│ └── src/ # Component source files
├── tools/ # Shared configurations
│ ├── eslint/
│ ├── prettier/
│ ├── tailwind/
│ └── typescript/
├── docker/ # Self-hosted deployment
│ ├── compose.yml
│ ├── Dockerfile
│ └── data/ # Convex data volume (gitignored in real use)
├── scripts/ # Docker/deployment helper scripts using root .env
│ ├── docker-compose
│ ├── build-next-app
│ ├── update-next-app
│ └── update-convex
├── turbo.json # Turborepo configuration
└── package.json # Root workspace & catalogs
```
---
## Features
### Authentication
- **OAuth:** Authentik SSO integration
- **Password:** Custom password auth with email verification
- **OTP:** Email verification via self-hosted UseSend
- **Session Management:** Secure cookie-based sessions (30-day max age)
### Next.js App
- **App Router:** Next.js 16 with React Server Components
- **Editable Marketing Content:** Payload-backed landing page with live preview
- **Data Preloading:** SSR data fetching with `preloadQuery` + `usePreloadedQuery`
- **Middleware:** Route protection & IP-based security (`src/proxy.ts`)
- **Styling:** Tailwind CSS v4 with dark mode (OKLCH-based theme)
- **Analytics:** Plausible (privacy-focused, proxied through Next.js)
- **Monitoring:** Sentry error tracking & performance
### Backend
- **Real-time:** Convex reactive queries
- **File Storage:** Built-in file upload/storage
- **Auth:** Multi-provider authentication
- **Type-safe:** Full TypeScript with generated types
- **Self-hosted:** Complete control over your data
### Developer Experience
- **Monorepo:** Turborepo for efficient builds and caching
- **Type Safety:** Strict TypeScript throughout
- **Code Quality:** ESLint + Prettier with auto-fix
- **Hot Reload:** Fast refresh for all packages
- **Catalog Deps:** Centralized dependency version management
---
## Deployment ## Deployment
### Production Deployment (Docker) Production Compose retains the self-hosted Convex backend/dashboard and accepts
an external Payload `PAYLOAD_DB_URL`. Its commented Postgres service remains an
Once the Convex containers are running (they only need to be started once), deploying optional Payload database. Gitea runs the quality gate first, builds the Next
a new version of the Next.js app is a one-command workflow: image from a temporary Gitea-secret env file, then pushes SHA and `latest` tags.
CI never installs or invokes Infisical.
```bash
./scripts/build-next-app
```
To start all services from scratch:
```bash
./scripts/docker-compose up -d backend dashboard
# Wait for backend health check to pass, then:
./scripts/docker-compose up -d next
```
**Services:**
- `next-app` — Next.js standalone build
- `convex-backend` — Convex backend (port 3210)
- `convex-dashboard` — Admin dashboard (port 6791)
**Network:** Uses `nginx-bridge` Docker network (reverse proxy via nginx-proxy-manager).
### Production Checklist
- [ ] Fill out root `/.env` with all app, Payload, and Docker Compose values
- [ ] Start `convex-backend` and `convex-dashboard` containers
- [ ] Generate and set `CONVEX_SELF_HOSTED_ADMIN_KEY` via `./scripts/generate-convex-admin-key`
- [ ] Reverse-proxy both Convex services via nginx-proxy-manager with SSL
- [ ] Generate JWT keys and sync all env vars to Convex (`bun with-env npx convex env set ...`)
- [ ] Update `CONVEX_SITE_URL` in the Convex Dashboard to your production Next.js URL
- [ ] Build and start the `next-app` container
- [ ] Back up `docker/data/` regularly (contains all Convex database data)
---
## Documentation
- **[AGENTS.md](./AGENTS.md)** — Comprehensive guide for AI agents & developers
- **[docs/payload-cms.md](./docs/payload-cms.md)** — How Payload is wired up and how to extend it in this template
- **[Convex Docs](https://docs.convex.dev)** — Official Convex documentation
- **[Turborepo Docs](https://turbo.build/repo/docs)** — Turborepo documentation
- **[Next.js Docs](https://nextjs.org/docs)** — Next.js documentation
---
## Troubleshooting
### Backend typecheck shows TypeScript help message
This is expected behavior. The backend package follows Convex's structure with only
`convex/tsconfig.json` (no root tsconfig). Running `bun typecheck` from the repo root
will show TypeScript's help text for `@gib/backend` — this is not an error.
### Imports from Convex require `.js` extension
The project uses ESM (`"type": "module"`), which requires explicit file extensions:
```typescript
// ✅ Correct
import type { Id } from '@gib/backend/convex/_generated/dataModel.js';
// ❌ Wrong — will fail at runtime
import { api } from '@gib/backend/convex/_generated/api';
import { api } from '@gib/backend/convex/_generated/api.js';
```
### Docker containers won't start
1. Check Docker logs: `./scripts/docker-compose logs`
2. Verify environment variables in root `/.env`
3. Ensure the `nginx-bridge` network exists: `sudo docker network create nginx-bridge`
4. Check that the required ports (3210, 6791) are not already in use
### Only one `.env` file matters
Use the root `/.env` for everything in this repo, including Docker helper scripts and
Docker Compose interpolation. Do not create or rely on a separate `docker/.env`.
### Auth doesn't work in production
Make sure `CONVEX_SITE_URL` is set to your production Next.js URL in the **Convex
Dashboard** (not just in the root `.env` file). The root `.env` should always contain
`http://localhost:3000`; the Dashboard must have your production URL.
### Catalog updates break workspace
After updating dependencies, if you see `sherif` errors on `bun install`:
1. Never use `bun update` inside individual package directories
2. Edit the version in root `package.json` catalog section instead
3. Run `bun install` from the root
4. Verify with `bun lint:ws`
---
## Contributing
This is a personal monorepo template. Feel free to fork and adapt for your needs.
### Code Style
- Single quotes, trailing commas
- 80 character line width
- ESLint + Prettier enforced
- `const fn = () => {}` over `function fn()` (strong preference)
- Import order: Types → React → Next → Third-party → @gib → Local
Run `bun lint:fix` and `bun format:fix` before committing.
---
## License
MIT
---
## Acknowledgments
Built with inspiration from:
- [T3 Turbo](https://github.com/t3-oss/create-t3-turbo)
- [Convex](https://convex.dev)
- [shadcn/ui](https://ui.shadcn.com)
- [Turborepo](https://turbo.build)
-1
View File
@@ -1 +0,0 @@
[{"/home/gib/Documents/Code/convex-monorepo/apps/expo/index.ts":"1","/home/gib/Documents/Code/convex-monorepo/apps/expo/nativewind-env.d.ts":"2","/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/_layout.tsx":"3","/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/index.tsx":"4","/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/post/[id].tsx":"5","/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/base-url.ts":"6","/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/convex.ts":"7","/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/session-store.ts":"8"},{"size":28,"mtime":1768155639000,"results":"9","hashOfConfig":"10"},{"size":246,"mtime":1766222924000,"results":"11","hashOfConfig":"10"},{"size":836,"mtime":1774546669443,"results":"12","hashOfConfig":"10"},{"size":1935,"mtime":1774546669443,"results":"13","hashOfConfig":"10"},{"size":678,"mtime":1774546669443,"results":"14","hashOfConfig":"10"},{"size":880,"mtime":1768155639000,"results":"15","hashOfConfig":"10"},{"size":909,"mtime":1774546669443,"results":"16","hashOfConfig":"10"},{"size":272,"mtime":1768155639000,"results":"17","hashOfConfig":"10"},{"filePath":"18","messages":"19","suppressedMessages":"20","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"x7pzu2",{"filePath":"21","messages":"22","suppressedMessages":"23","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"24","messages":"25","suppressedMessages":"26","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"27","messages":"28","suppressedMessages":"29","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"30","messages":"31","suppressedMessages":"32","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"33","messages":"34","suppressedMessages":"35","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"36","messages":"37","suppressedMessages":"38","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"39","messages":"40","suppressedMessages":"41","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/home/gib/Documents/Code/convex-monorepo/apps/expo/index.ts",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/nativewind-env.d.ts",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/_layout.tsx",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/index.tsx",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/post/[id].tsx",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/base-url.ts",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/convex.ts",[],[],"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/session-store.ts",[],[]]
-1
View File
@@ -1 +0,0 @@
[["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22"],{"key":"23","value":"24"},{"key":"25","value":"26"},{"key":"27","value":"28"},{"key":"29","value":"30"},{"key":"31","value":"32"},{"key":"33","value":"34"},{"key":"35","value":"36"},{"key":"37","value":"38"},{"key":"39","value":"40"},{"key":"41","value":"42"},{"key":"43","value":"44"},{"key":"45","value":"46"},{"key":"47","value":"48"},{"key":"49","value":"50"},{"key":"51","value":"52"},{"key":"53","value":"54"},{"key":"55","value":"56"},{"key":"57","value":"58"},{"key":"59","value":"60"},{"key":"61","value":"62"},{"key":"63","value":"64"},{"key":"65","value":"66"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/index.tsx",{"size":1935,"mtime":1774546669443,"hash":"67","data":"68"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/index.ts",{"size":28,"mtime":1768155639000,"hash":"69","data":"70"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-dark.png",{"size":19633,"mtime":1766222924000,"hash":"71"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/postcss.config.js",{"size":65,"mtime":1774546669443,"hash":"72","data":"73"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/post/[id].tsx",{"size":678,"mtime":1774546669443,"hash":"74","data":"75"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/convex.ts",{"size":909,"mtime":1774546669443,"data":"76"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eas.json",{"size":566,"mtime":1774546669443,"hash":"77","data":"78"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/app/_layout.tsx",{"size":836,"mtime":1774546669443,"hash":"79","data":"80"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/styles.css",{"size":89,"mtime":1774546669443,"hash":"81","data":"82"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.cache/.prettiercache",{"size":4789,"mtime":1774718093704},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.cache/.eslintcache",{"size":3079,"mtime":1774717490445},"/home/gib/Documents/Code/convex-monorepo/apps/expo/assets/icon-light.png",{"size":19133,"mtime":1766222924000,"hash":"83"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/package.json",{"size":2228,"mtime":1774588990619,"hash":"84","data":"85"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/turbo.json",{"size":171,"mtime":1774031879321,"hash":"86","data":"87"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/eslint.config.mts",{"size":274,"mtime":1774546669443,"hash":"88","data":"89"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/tsconfig.json",{"size":387,"mtime":1766228480000,"hash":"90","data":"91"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/metro.config.js",{"size":511,"mtime":1768155639000,"hash":"92","data":"93"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/nativewind-env.d.ts",{"size":246,"mtime":1766222924000,"hash":"94","data":"95"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/base-url.ts",{"size":880,"mtime":1768155639000,"hash":"96","data":"97"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/.expo-shared/assets.json",{"size":155,"mtime":1766222924000,"hash":"98","data":"99"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/src/utils/session-store.ts",{"size":272,"mtime":1768155639000,"hash":"100","data":"101"},"/home/gib/Documents/Code/convex-monorepo/apps/expo/app.config.ts",{"size":1333,"mtime":1768155639000,"hash":"102","data":"103"},"73c235a66242df70b69394cce29d1ed3",{"hashOfOptions":"104"},"11cdbef6afa001cd39bc187041ca6865",{"hashOfOptions":"105"},"1e8ac0d261e95efb19d290ffcf70ce36","b7edffce093c4c84092cc93f3dc208ef",{"hashOfOptions":"106"},"ead19d73283f9d8e08b55c896c9fd570",{"hashOfOptions":"107"},{"hashOfOptions":"108"},"a3c1487f8318513ae7c156acc857fde2",{"hashOfOptions":"109"},"8e407b4b1b0c0bd9c862a00243344be3",{"hashOfOptions":"110"},"52a1d72379b952dd802f47e1865bd0da",{"hashOfOptions":"111"},"863da15dbd856008b7c24077ca746d91","d8763702c14cdc382dcfb84f6f9a068f",{"hashOfOptions":"112"},"c7d4dcf839dfeaa02e0407adfd5e47a6",{"hashOfOptions":"113"},"1c1710ce3de3ce02e8054cc3787c8579",{"hashOfOptions":"114"},"6937fb7370f1e17491df649888d6ecc9",{"hashOfOptions":"115"},"dbe97bcde588a81538bbcd6a9befdddd",{"hashOfOptions":"116"},"d4d589c153ac8b5e7bf0fb130a5b5a7d",{"hashOfOptions":"117"},"dd2007a211e323deabb3f7fa7d16313f",{"hashOfOptions":"118"},"0f7f54c7161b8403d3bc42d91f59cd91",{"hashOfOptions":"119"},"1bc3e15a40c117eecc51294886ea9b38",{"hashOfOptions":"120"},"4f49c6df7733f874fbe72b4e20b3092b",{"hashOfOptions":"121"},"3000879843","3103968608","384110377","141502567","1235541372","1050155876","2025343866","4147067111","4228440757","3451484829","4039211292","3318113268","2585374463","45764855","1418614640","2754339483","1950506033","3468872477"]
+6 -5
View File
@@ -4,15 +4,16 @@
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {
"clean": "git clean -xdf .cache .expo .turbo android ios node_modules", "clean": "git clean -xdf .cache .expo .turbo android ios node_modules",
"dev": "expo start", "dev": "bun with-env expo start",
"dev:tunnel": "expo start --tunnel", "dev:tunnel": "bun with-env expo start --tunnel",
"dev:android": "expo start --android", "dev:android": "bun with-env expo start --android",
"dev:ios": "expo start --ios", "dev:ios": "bun with-env expo start --ios",
"android": "expo run:android", "android": "expo run:android",
"ios": "expo run:ios", "ios": "expo run:ios",
"format": "prettier --check . --ignore-path ../../.gitignore --ignore-path .prettierignore", "format": "prettier --check . --ignore-path ../../.gitignore --ignore-path .prettierignore",
"lint": "eslint --flag unstable_native_nodejs_ts_config", "lint": "eslint --flag unstable_native_nodejs_ts_config",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit",
"with-env": "sh ../../scripts/with-env ${INFISICAL_ENV:-dev} --"
}, },
"dependencies": { "dependencies": {
"@convex-dev/auth": "catalog:convex", "@convex-dev/auth": "catalog:convex",
+1
View File
@@ -0,0 +1 @@
declare module '*.css';
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -6,7 +6,7 @@ import { reactConfig } from '@gib/eslint-config/react';
export default defineConfig( export default defineConfig(
{ {
ignores: ['.next/**'], ignores: ['.next/**', 'payload-types.ts'],
}, },
baseConfig, baseConfig,
reactConfig, reactConfig,
+10 -3
View File
@@ -5,7 +5,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"build": "bun with-env next build", "build": "bun with-env next build",
"build:env": "bun with-env next build", "build:docker": "next build --webpack",
"clean": "git clean -xdf .cache .next .turbo node_modules", "clean": "git clean -xdf .cache .next .turbo node_modules",
"dev": "bun with-env next dev --turbo", "dev": "bun with-env next dev --turbo",
"dev:tunnel": "bun with-env next dev --turbo", "dev:tunnel": "bun with-env next dev --turbo",
@@ -14,7 +14,10 @@
"lint": "eslint --flag unstable_native_nodejs_ts_config", "lint": "eslint --flag unstable_native_nodejs_ts_config",
"start": "bun with-env next start", "start": "bun with-env next start",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --" "test:unit": "NODE_ENV=test vitest run --project unit",
"test:integration": "NODE_ENV=test vitest run --project integration",
"test:component": "NODE_ENV=test vitest run --project component",
"with-env": "sh ../../scripts/with-env ${INFISICAL_ENV:-dev} --"
}, },
"dependencies": { "dependencies": {
"@convex-dev/auth": "catalog:convex", "@convex-dev/auth": "catalog:convex",
@@ -41,15 +44,19 @@
"@gib/prettier-config": "workspace:*", "@gib/prettier-config": "workspace:*",
"@gib/tailwind-config": "workspace:*", "@gib/tailwind-config": "workspace:*",
"@gib/tsconfig": "workspace:*", "@gib/tsconfig": "workspace:*",
"@gib/vitest-config": "workspace:*",
"@testing-library/react": "catalog:test",
"@types/node": "catalog:", "@types/node": "catalog:",
"@types/react": "catalog:react19", "@types/react": "catalog:react19",
"@types/react-dom": "catalog:react19", "@types/react-dom": "catalog:react19",
"baseline-browser-mapping": "^2.10.11", "baseline-browser-mapping": "^2.10.11",
"eslint": "catalog:", "eslint": "catalog:",
"jsdom": "catalog:test",
"prettier": "catalog:", "prettier": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "catalog:" "typescript": "catalog:",
"vitest": "catalog:test"
}, },
"prettier": "@gib/prettier-config" "prettier": "@gib/prettier-config"
} }
-1
View File
@@ -1,4 +1,3 @@
/* tslint:disable */
/* eslint-disable */ /* eslint-disable */
/** /**
* This file was automatically generated by Payload. * This file was automatically generated by Payload.
@@ -19,8 +19,7 @@ export const generateMetadata = (): Metadata => {
const ProfileLayout = async ({ const ProfileLayout = async ({
children, children,
}: Readonly<{ children: React.ReactNode }>) => { }: Readonly<{ children: React.ReactNode }>) => {
if (!(await isAuthenticatedNextjs())) if (!(await isAuthenticatedNextjs())) redirect('/sign-in');
redirect('/sign-in');
return <>{children}</>; return <>{children}</>;
}; };
export default ProfileLayout; export default ProfileLayout;
+1
View File
@@ -0,0 +1 @@
declare module '@payloadcms/next/css';
+11
View File
@@ -0,0 +1,11 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
const Greeting = ({ name }: { name: string }) => <p>Hello {name}</p>;
describe('component test harness', () => {
it('renders through jsdom and Testing Library', () => {
render(<Greeting name='world' />);
expect(screen.getByText('Hello world')).toBeInTheDocument();
});
});
@@ -0,0 +1,7 @@
import { describe, expect, it } from 'vitest';
describe('integration test harness', () => {
it('provides modern Node globals', () => {
expect(typeof globalThis.structuredClone).toBe('function');
});
});
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';
+7
View File
@@ -0,0 +1,7 @@
import { describe, expect, it } from 'vitest';
describe('unit test harness', () => {
it('executes isolated assertions', () => {
expect(1 + 1).toBe(2);
});
});
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from 'vitest/config';
import { jsdomProject, nodeProject } from '@gib/vitest-config';
export default defineConfig({
test: {
projects: [
nodeProject('unit', ['tests/unit/**/*.test.{ts,tsx}']),
nodeProject('integration', ['tests/integration/**/*.test.{ts,tsx}']),
jsdomProject('component', ['tests/component/**/*.test.{ts,tsx}']),
],
},
});
+384 -35
View File
File diff suppressed because it is too large Load Diff
+29 -4
View File
@@ -1,24 +1,49 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM oven/bun:alpine AS base FROM docker.io/oven/bun:1.3.10-alpine AS base
# Builder stage # Builder stage
FROM base AS builder FROM base AS builder
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app 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_SENTRY_DSN
ARG NEXT_PUBLIC_SENTRY_URL
ARG NEXT_PUBLIC_SENTRY_ORG
ARG NEXT_PUBLIC_SENTRY_PROJECT_NAME
ARG PAYLOAD_SECRET
ARG PAYLOAD_DB_URL
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_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN
ENV NEXT_PUBLIC_SENTRY_URL=$NEXT_PUBLIC_SENTRY_URL
ENV NEXT_PUBLIC_SENTRY_ORG=$NEXT_PUBLIC_SENTRY_ORG
ENV NEXT_PUBLIC_SENTRY_PROJECT_NAME=$NEXT_PUBLIC_SENTRY_PROJECT_NAME
ENV PAYLOAD_SECRET=$PAYLOAD_SECRET
ENV PAYLOAD_DB_URL=$PAYLOAD_DB_URL
# Copy source code (node_modules excluded via .dockerignore) # Copy source code (node_modules excluded via .dockerignore)
COPY . . COPY . .
# Install all dependencies # Install all dependencies
RUN bun install ENV HUSKY=0
RUN bun install --frozen-lockfile
# Build with proper environment # Build with proper environment
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production ENV NODE_ENV=production
RUN bun run build --filter=@gib/next RUN cd apps/next && bun run build:docker
# Runner stage # Runner stage
FROM node:22-alpine AS runner FROM docker.io/library/node:22-alpine AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
+62
View File
@@ -0,0 +1,62 @@
name: convexmonorepo-local
services:
postgres:
image: postgres:17
container_name: convexmonorepo-local-postgres
ports: ['${POSTGRES_PORT:-5432}:5432']
environment:
- POSTGRES_USER=${POSTGRES_USER:-convexmonorepo}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-localdev}
- POSTGRES_DB=${POSTGRES_DB:-convexmonorepo_payload}
volumes: [payload-postgres-data:/var/lib/postgresql/data]
restart: unless-stopped
healthcheck:
test: ['CMD-SHELL', 'pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}']
start_period: 10s
interval: 5s
retries: 10
timeout: 5s
convex-backend:
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'
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}
- DISABLE_BEACON=true
- REDACT_LOGS_TO_CLIENT=false
- DO_NOT_REQUIRE_SSL=true
# Convex uses its own volume by default. A cloned project may opt into
# Convex-on-Postgres by configuring a separate database URL here:
# - POSTGRES_URL=postgres://user:password@postgres:5432/convex?sslmode=disable
volumes: [convex-data:/convex/data]
restart: unless-stopped
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3210/version']
start_period: 10s
interval: 5s
retries: 20
timeout: 5s
stop_grace_period: 10s
stop_signal: SIGINT
convex-dashboard:
image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
container_name: convexmonorepo-local-convex-dashboard
ports: ['${DASHBOARD_PORT:-6791}:6791']
environment:
- NEXT_PUBLIC_DEPLOYMENT_URL=http://localhost:${BACKEND_PORT:-3210}
depends_on:
convex-backend:
condition: service_healthy
restart: unless-stopped
volumes:
payload-postgres-data:
convex-data:
+38 -23
View File
@@ -7,7 +7,19 @@ services:
build: build:
context: ../ context: ../
dockerfile: ./docker/Dockerfile dockerfile: ./docker/Dockerfile
image: ${NEXT_CONTAINER_NAME}:latest args:
SENTRY_AUTH_TOKEN: ${SENTRY_AUTH_TOKEN}
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_SENTRY_DSN: ${NEXT_PUBLIC_SENTRY_DSN}
NEXT_PUBLIC_SENTRY_URL: ${NEXT_PUBLIC_SENTRY_URL}
NEXT_PUBLIC_SENTRY_ORG: ${NEXT_PUBLIC_SENTRY_ORG}
NEXT_PUBLIC_SENTRY_PROJECT_NAME: ${NEXT_PUBLIC_SENTRY_PROJECT_NAME}
PAYLOAD_SECRET: ${PAYLOAD_SECRET}
PAYLOAD_DB_URL: ${PAYLOAD_DB_URL}
image: convexmonorepo-next:latest
#image: git.gbrown.org/gib/${NEXT_CONTAINER_NAME}:latest #image: git.gbrown.org/gib/${NEXT_CONTAINER_NAME}:latest
container_name: ${NEXT_CONTAINER_NAME} container_name: ${NEXT_CONTAINER_NAME}
environment: environment:
@@ -43,12 +55,13 @@ services:
labels: ['com.centurylinklabs.watchtower.enable=true'] labels: ['com.centurylinklabs.watchtower.enable=true']
environment: environment:
- INSTANCE_NAME=${INSTANCE_NAME} - INSTANCE_NAME=${INSTANCE_NAME}
#- INSTANCE_SECRET - INSTANCE_SECRET=${INSTANCE_SECRET}
- CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${BACKEND_PORT:-3210}} - CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${BACKEND_PORT:-3210}}
- CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${SITE_PROXY_PORT:-3211}} - CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-stpeteit-backend}:${SITE_PROXY_PORT:-3211}}
- DISABLE_BEACON=${DISABLE_BEACON:-true} - DISABLE_BEACON=${DISABLE_BEACON:-true}
- REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-true} - REDACT_LOGS_TO_CLIENT=${REDACT_LOGS_TO_CLIENT:-true}
- DO_NOT_REQUIRE_SSL=${DO_NOT_REQUIRE_SSL:-false} - DO_NOT_REQUIRE_SSL=${DO_NOT_REQUIRE_SSL:-false}
# Optional: Convex-on-Postgres is not the template default.
#- POSTGRES_URL=${POSTGRES_URL} #- POSTGRES_URL=${POSTGRES_URL}
stdin_open: true stdin_open: true
tty: true tty: true
@@ -80,25 +93,27 @@ services:
stop_grace_period: 10s stop_grace_period: 10s
stop_signal: SIGINT stop_signal: SIGINT
# Optional production Postgres. Payload may instead continue using an
# external/VPN PAYLOAD_DB_URL. Convex does not share this DB by default.
#convexmonorepo-postgresql: #convexmonorepo-postgresql:
#image: postgres:17 #image: postgres:17
#container_name: ${POSTGRES_CONTAINER_NAME:-convexmonorepo-postgres} #container_name: ${POSTGRES_CONTAINER_NAME:-convexmonorepo-postgres}
#hostname: ${POSTGRES_CONTAINER_NAME:-convexmonorepo-postgres} #hostname: ${POSTGRES_CONTAINER_NAME:-convexmonorepo-postgres}
#domainname: postgres.${NEXT_DOMAIN:-convexmonorepo.gbrown.org} #domainname: postgres.${NEXT_DOMAIN:-convexmonorepo.gbrown.org}
#networks: ['${NETWORK:-nginx-bridge}'] #networks: ['${NETWORK:-nginx-bridge}']
#ports: ['5432:5432'] #ports: ['5432:5432']
#environment: #environment:
#- POSTGRES_USER=${POSTGRES_USER:-convexmonorepo} #- POSTGRES_USER=${POSTGRES_USER:-convexmonorepo}
#- POSTGRES_PASSWORD=${POSTGRES_PASSWORD} #- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
#- POSTGRES_DB=${POSTGRES_DB:-convexmonorepo_payload} #- POSTGRES_DB=${POSTGRES_DB:-convexmonorepo_payload}
#labels: ['com.centurylinklabs.watchtower.enable=true'] #labels: ['com.centurylinklabs.watchtower.enable=true']
#volumes: ['./volumes/postgres:/var/lib/postgresql/data'] #volumes: ['./volumes/postgres:/var/lib/postgresql/data']
#tty: true #tty: true
#stdin_open: true #stdin_open: true
#restart: unless-stopped #restart: unless-stopped
#healthcheck: #healthcheck:
#test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] #test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
#start_period: 20s #start_period: 20s
#interval: 30s #interval: 30s
#retries: 5 #retries: 5
#timeout: 5s #timeout: 5s
+39 -419
View File
@@ -1,424 +1,44 @@
# Payload CMS In `convex-monorepo` # Payload CMS
This document explains how Payload CMS is integrated into this template, what it manages Payload is embedded in `apps/next` and manages marketing content. Convex remains
today, and how to extend it safely when you want more editable marketing content. the application backend for auth, realtime data, files, and workflows.
## Current Scope Configuration lives in `apps/next/src/payload.config.ts`. Payload uses
`PAYLOAD_SECRET`, `PAYLOAD_DB_URL`, and `NEXT_PUBLIC_SITE_URL` from the validated
Payload currently powers the editable marketing layer inside the Next.js app. environment contract. Local `dev` values come from Infisical and point Payload
to Postgres at `localhost:5432`, because Next runs on the host.
Today that means:
Start the isolated services with `bun db:up`, then run `bun dev:next`. The local
- the landing page at `/` database persists across `bun db:down`; `bun db:down:wipe` removes it. Local
- the admin UI at `/admin` Convex data is stored in a separate volume and Convex does not receive
- the REST API under `/api` `POSTGRES_URL` by default.
- the GraphQL API under `/api/graphql`
- live preview for the landing page To refresh the local seed snapshot from current staging/production-derived
Payload content, stop Next and run `bun db:sync:payload`. The command uses
Payload is not replacing Convex. staging only as a read-only dump source, verifies the restore destination is
localhost, writes `.local/payload-staging.dump`, and force-applies it locally.
- Convex still handles auth, backend logic, realtime data, files, and app workflows Normal `bun db:up` never contacts staging; it only restores that local snapshot
- Payload owns marketing content editing inside the Next app when `.local/payload-seed-state.env` is absent or does not match the current
snapshot. The marker stays outside Postgres so Payload schema push does not see
## High-Level Architecture an unmanaged table. The snapshot is gitignored and contains sensitive
production-derived records, including Payload users and password hashes. Do not
### Core config share or commit it.
Payload is configured in `apps/next/src/payload.config.ts`. The public landing page is in `apps/next/src/app/(frontend)/page.tsx`; schemas
are under `apps/next/src/payload`, and rendering components are under
Important pieces there: `apps/next/src/components/landing`. Custom routes belong in `(frontend)`, not
the generated `(payload)` route group.
- `postgresAdapter(...)` connects Payload to Postgres via `PAYLOAD_DB_URL`
- `secret: env.PAYLOAD_SECRET` secures Payload Do not manually edit Payload-generated routes, layouts, import maps, or
- `collections: [Users]` currently registers only the Payload admin user collection `apps/next/payload-types.ts`. When schema changes require regeneration, run the
- `globals: [LandingPage]` registers the editable landing-page global Payload generators through the package's environment wrapper:
- `lexicalEditor()` enables the Payload editor setup
```sh
### Current Payload data model
This template is intentionally small right now.
Current Payload entities:
- `users` collection in `apps/next/src/payload/collections/users.ts`
- used by Payload admin itself
- `landing-page` global in `apps/next/src/payload/globals/landing-page.ts`
- stores the homepage layout as a block list
There is no `pages` collection in this template yet.
That means the current pattern is:
- one global for one marketing page
- one frontend route that reads that global
- reusable block schemas that editors can reorder inside the global
## How the Landing Page Works
### Schema side
The landing page is defined by:
- `apps/next/src/payload/globals/landing-page.ts`
- `apps/next/src/payload/globals/landing-page-blocks.ts`
`landing-page.ts` defines a single global with a `layout` field of type `blocks`.
`landing-page-blocks.ts` defines the actual editable block types, including:
- `hero`
- `logoCloud`
- `features`
- `stats`
- `techStack`
- `testimonials`
- `pricing`
- `faq`
- `cta`
### Frontend side
The frontend route is:
- `apps/next/src/app/(frontend)/page.tsx`
That route calls:
- `apps/next/src/lib/payload/get-landing-page-content.ts`
That helper fetches the `landing-page` global from Payload and merges it with fallback
content from:
- `apps/next/src/components/landing/content.ts`
That fallback layer is important. It means the page can still render even if:
- the Payload DB is empty
- an editor saves partial content
- a newly added field is missing from older content
### Rendering flow
The homepage flow is:
1. `/` loads in `apps/next/src/app/(frontend)/page.tsx`
2. the page checks whether `?preview=true` is enabled
3. `getLandingPageContent(isPreview)` fetches the `landing-page` global
4. the fetched global is merged with defaults from `apps/next/src/components/landing/content.ts`
5. `LandingPageBuilder` renders the normalized block data
6. `RefreshRouteOnSave` keeps preview mode refreshed after saves
## Live Preview
Live preview is configured in:
- `apps/next/src/payload/globals/landing-page.ts`
The preview URL is:
- `/?preview=true`
The frontend bridge is:
- `apps/next/src/components/payload/refresh-route-on-save.tsx`
That component uses Payload's live-preview utilities plus Next's router refresh so
saved changes show up in the preview iframe.
Important requirement:
- `NEXT_PUBLIC_SITE_URL` must point to the correct frontend origin
If preview appears blank, stale, or disconnected, that is one of the first values to
check.
## Environment Variables
Payload depends on these env vars:
- `PAYLOAD_SECRET`
- `PAYLOAD_DB_URL`
- `NEXT_PUBLIC_SITE_URL`
Why they matter:
- `PAYLOAD_SECRET` secures Payload
- `PAYLOAD_DB_URL` connects Payload to Postgres
- `NEXT_PUBLIC_SITE_URL` is used by live preview and frontend URL generation
All of them live in the single root `/.env` file.
## Adding a New Landing-Page Block
If you want editors to control a new section type, add a new block.
### 1. Add the block schema
Update:
- `apps/next/src/payload/globals/landing-page-blocks.ts`
Add a new block object to the `landingPageBlocks` array.
### 2. Extend the frontend content types
Update:
- `apps/next/src/components/landing/content.ts`
You usually need to:
- add the new block TypeScript shape
- add default content for it
- add sanitizing / merging logic for it
- include it in the landing-page block union
### 3. Teach the landing-page builder to render it
Update the landing-page rendering layer in the landing components so the new block type
actually appears on the page.
If you add schema without renderer support, editors can save the block but the frontend
will not know what to do with it.
### 4. Regenerate generated Payload files if needed
Useful commands:
```bash
cd apps/next cd apps/next
bun with-env bunx payload generate:types --config src/payload.config.ts bun with-env bunx payload generate:types --config src/payload.config.ts
bun with-env bunx payload generate:db-schema --config src/payload.config.ts bun with-env bunx payload generate:db-schema --config src/payload.config.ts
``` ```
That refreshes: For staging, use `INFISICAL_ENV=staging`; CI uses temporary Gitea-injected
configuration and never Infisical. Production Payload may use the existing
- `apps/next/payload-types.ts` external/VPN Postgres URL or the commented optional Compose Postgres service.
- `apps/next/src/payload-generated-schema.ts`
Do not hand-edit those files.
## Copy-Paste Block Template
Use this as a starting point when you want a new landing-page block.
```ts
{
slug: 'exampleBlock',
labels: {
singular: 'Example Block',
plural: 'Example Blocks',
},
fields: [
{
name: 'heading',
type: 'text',
required: true,
},
{
name: 'description',
type: 'textarea',
},
{
name: 'items',
type: 'array',
fields: [
{
name: 'label',
type: 'text',
required: true,
},
],
},
],
}
```
After adding it:
1. add the block to `landingPageBlocks`
2. add the TypeScript/content shape in `apps/next/src/components/landing/content.ts`
3. add fallback content and sanitizers
4. add rendering support in the landing components
## Copy-Paste Global-Backed Route Template
Use this when you want another singular Payload-managed page in this template and you do
not need a full `pages` collection yet.
This is the best next step for one-off pages like `about`, `contact`, or `pricing`.
```tsx
import { draftMode } from 'next/headers';
import { notFound } from 'next/navigation';
import { RefreshRouteOnSave } from '@/components/payload/refresh-route-on-save';
import { getPayloadClient } from '@/lib/payload/get-payload';
const AboutPage = async () => {
const { isEnabled: isPreview } = await draftMode();
const payload = await getPayloadClient();
const aboutPage = await payload.findGlobal({
slug: 'about-page',
draft: isPreview,
});
if (!aboutPage) return notFound();
return (
<main>
{isPreview ? <RefreshRouteOnSave /> : null}
{/* render the normalized Payload data here */}
</main>
);
};
export default AboutPage;
```
For this pattern you would also:
1. add a new global in `apps/next/src/payload/globals/`
2. register it in `apps/next/src/payload.config.ts`
3. create the fetch/normalize helper in `apps/next/src/lib/payload/`
4. create fallback content in the relevant component/content files
## When To Add Another Global vs. A `pages` Collection
### Add another global when:
- the page is singular
- the route is fixed
- the content model is custom
- you only need a few editable marketing pages
Examples:
- homepage
- contact page
- about page
- pricing page
### Add a `pages` collection when:
- you need many CMS-managed pages
- page routing should be slug-driven
- many pages share the same block system
- editors should be able to create pages without new code for each route
Examples:
- service pages
- case studies
- marketing subpages
- partner pages
## How To Migrate Another Hardcoded Page To Payload In This Repo
Because this template currently uses a global-backed landing page, the most natural next
migration path is usually another Payload global.
### Step 1: Audit the current page
Identify:
- route path
- metadata
- JSON-LD or structured data
- reusable sections
- what should stay hardcoded vs. what should become editable
### Step 2: Decide the storage model
Ask:
- is this a one-off page? use another global
- is this the beginning of many similar pages? consider creating a `pages` collection
### Step 3: Model the editable fields
Prefer structured fields and blocks over dumping everything into one rich text area.
### Step 4: Add fallback content
Keep the same resilience pattern used by the current landing page:
- define defaults in component/content files
- merge Payload content into those defaults
This avoids blank pages when content is incomplete.
### Step 5: Wire the route
Create a server route in `apps/next/src/app/(frontend)/...` that:
1. reads preview state
2. fetches Payload data
3. normalizes or merges it
4. renders the page
5. includes `RefreshRouteOnSave` in preview mode
### Step 6: Verify preview and admin flow
After migration, verify:
- the route renders normally
- the page updates from the admin
- preview refresh works
- the page still renders when content is partial
## Common Failure Modes
### 1. Homepage renders old or partial content
Check:
- whether the landing-page global actually saved
- whether preview mode is enabled
- whether your fallback merge logic is masking a missing field
### 2. Live preview does not refresh
Check:
- `NEXT_PUBLIC_SITE_URL`
- `apps/next/src/components/payload/refresh-route-on-save.tsx`
- the `admin.livePreview.url` value in the Payload global config
### 3. New block fields do not show up in the frontend
Usually means the schema changed but the frontend data contract did not.
Check:
- `apps/next/src/payload/globals/landing-page-blocks.ts`
- `apps/next/src/components/landing/content.ts`
- the landing-page render path
### 4. Payload admin works but the page looks empty
Usually means one of these:
- the block renderer support is missing
- the global returned data that your merge layer is not handling yet
- the page route is not reading the correct global or preview state
## Important Files At A Glance
- config: `apps/next/src/payload.config.ts`
- payload global: `apps/next/src/payload/globals/landing-page.ts`
- landing-page block schemas: `apps/next/src/payload/globals/landing-page-blocks.ts`
- payload client helper: `apps/next/src/lib/payload/get-payload.ts`
- landing-page fetch helper: `apps/next/src/lib/payload/get-landing-page-content.ts`
- landing-page content defaults: `apps/next/src/components/landing/content.ts`
- landing-page route: `apps/next/src/app/(frontend)/page.tsx`
- preview refresh bridge: `apps/next/src/components/payload/refresh-route-on-save.tsx`
- generated types: `apps/next/payload-types.ts`
- generated db schema: `apps/next/src/payload-generated-schema.ts`
## Practical Rule Of Thumb
If the task is “make marketing content editable,” reach for Payload.
If the task is “build application logic, auth, data workflows, or realtime product
features,” it probably belongs in Convex instead.
+58 -2
View File
@@ -21,6 +21,18 @@
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"catalogs": { "catalogs": {
"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/user-event": "^14.6.1",
"@vitejs/plugin-react": "^6.0.2",
"@vitest/coverage-v8": "^4.1.8",
"convex-test": "^0.0.53",
"jsdom": "^29.1.1",
"vitest": "^4.1.8"
},
"convex": { "convex": {
"@convex-dev/auth": "^0.0.87", "@convex-dev/auth": "^0.0.87",
"convex": "^1.34.1" "convex": "^1.34.1"
@@ -42,7 +54,13 @@
"dev:next:web": "turbo run dev:web -F @gib/next -F @gib/backend", "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:expo": "turbo run dev -F @gib/expo -F @gib/backend",
"dev:backend": "turbo run dev -F @gib/backend", "dev:backend": "turbo run dev -F @gib/backend",
"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", "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: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", "format": "turbo run format --continue -- --cache --cache-location .cache/.prettiercache",
"format:fix": "turbo run format --continue -- --write --cache --cache-location .cache/.prettiercache", "format:fix": "turbo run format --continue -- --write --cache --cache-location .cache/.prettiercache",
"lint": "turbo run lint --continue -- --cache --cache-location .cache/.eslintcache", "lint": "turbo run lint --continue -- --cache --cache-location .cache/.eslintcache",
@@ -51,19 +69,57 @@
"patch:usesend": "node scripts/patch-usesend.mjs", "patch:usesend": "node scripts/patch-usesend.mjs",
"postinstall": "bun patch:usesend && bun lint:ws", "postinstall": "bun patch:usesend && bun lint:ws",
"typecheck": "turbo run typecheck", "typecheck": "turbo run typecheck",
"test": "turbo run test:unit test:integration test:component",
"test:unit": "turbo run test:unit",
"test:integration": "turbo run test:integration",
"test:component": "turbo run test:component",
"test:e2e": "bash scripts/e2e",
"test:all": "turbo run test:unit test:integration test:component && bun test:e2e",
"ci:check": "bun lint:ws && turbo run lint typecheck test:unit test:integration test:component --concurrency=2 && bun test:e2e",
"prepare": "husky",
"ui-add": "turbo run ui-add", "ui-add": "turbo run ui-add",
"android": "expo run:android", "android": "expo run:android",
"ios": "expo run:ios" "ios": "expo run:ios"
}, },
"devDependencies": { "devDependencies": {
"@gib/prettier-config": "workspace:", "@gib/prettier-config": "workspace:",
"@turbo/gen": "^2.8.21", "@turbo/gen": "^2.9.18",
"dotenv-cli": "11.0.0", "dotenv-cli": "11.0.0",
"husky": "^9.1.7",
"lint-staged": "^17.0.7",
"prettier": "catalog:", "prettier": "catalog:",
"turbo": "^2.8.21", "turbo": "^2.9.18",
"typescript": "catalog:" "typescript": "catalog:"
}, },
"prettier": "@gib/prettier-config", "prettier": "@gib/prettier-config",
"lint-staged": {
"apps/next/**/*.{ts,tsx}": [
"eslint --flag unstable_native_nodejs_ts_config --fix --no-warn-ignored --config apps/next/eslint.config.ts",
"prettier --write"
],
"apps/expo/**/*.{ts,tsx}": [
"eslint --flag unstable_native_nodejs_ts_config --fix --no-warn-ignored --config apps/expo/eslint.config.mts",
"prettier --write"
],
"packages/backend/**/*.{ts,tsx}": [
"eslint --flag unstable_native_nodejs_ts_config --fix --no-warn-ignored --config packages/backend/eslint.config.ts",
"prettier --write"
],
"packages/ui/**/*.{ts,tsx}": [
"eslint --flag unstable_native_nodejs_ts_config --fix --no-warn-ignored --config packages/ui/eslint.config.ts",
"prettier --write"
],
"tools/tailwind/**/*.{ts,tsx}": [
"eslint --flag unstable_native_nodejs_ts_config --fix --no-warn-ignored --config tools/tailwind/eslint.config.ts",
"prettier --write"
],
"tools/{eslint,prettier,typescript,vitest}/**/*.{ts,tsx}": [
"prettier --write"
],
"**/*.{js,mjs,cjs,md,json,yaml,yml,css}": [
"prettier --write"
]
},
"trustedDependencies": [ "trustedDependencies": [
"@sentry/cli", "@sentry/cli",
"core-js-pure", "core-js-pure",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4 -5
View File
@@ -11,12 +11,11 @@
import type { import type {
DataModelFromSchemaDefinition, DataModelFromSchemaDefinition,
DocumentByName, DocumentByName,
SystemTableNames,
TableNamesInDataModel, TableNamesInDataModel,
} from 'convex/server'; SystemTableNames,
import type { GenericId } from 'convex/values'; } from "convex/server";
import type { GenericId } from "convex/values";
import schema from '../schema.js'; import schema from "../schema.js";
/** /**
* The names of all of your Convex tables. * The names of all of your Convex tables.
@@ -11,16 +11,16 @@ export default function UseSendProvider(config: EmailUserConfig): EmailConfig {
from: process.env.USESEND_FROM_EMAIL ?? 'noreply@example.com', from: process.env.USESEND_FROM_EMAIL ?? 'noreply@example.com',
maxAge: 24 * 60 * 60, // 24 hours maxAge: 24 * 60 * 60, // 24 hours
generateVerificationToken() { generateVerificationToken: () => {
const random: RandomReader = { const random: RandomReader = {
read(bytes) { read: (bytes) => {
crypto.getRandomValues(bytes); crypto.getRandomValues(bytes as Uint8Array<ArrayBuffer>);
}, },
}; };
return generateRandomString(random, alphabet('0-9'), 6); return generateRandomString(random, alphabet('0-9'), 6);
}, },
async sendVerificationRequest(params) { sendVerificationRequest: async (params) => {
const { identifier: to, provider, url, token } = params; const { identifier: to, provider, url, token } = params;
// Derive a display name from the site URL, fallback to 'App' // Derive a display name from the site URL, fallback to 'App'
const siteUrl = process.env.USESEND_FROM_EMAIL ?? ''; const siteUrl = process.env.USESEND_FROM_EMAIL ?? '';
+9 -2
View File
@@ -20,7 +20,10 @@
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint --flag unstable_native_nodejs_ts_config", "lint": "eslint --flag unstable_native_nodejs_ts_config",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --" "test:unit": "vitest run --project unit",
"test:integration": "vitest run --project integration --passWithNoTests",
"test:component": "vitest run --project component --passWithNoTests",
"with-env": "sh ../../scripts/with-env ${INFISICAL_ENV:-dev} --"
}, },
"dependencies": { "dependencies": {
"@oslojs/crypto": "^1.0.1", "@oslojs/crypto": "^1.0.1",
@@ -33,14 +36,18 @@
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@edge-runtime/vm": "catalog:test",
"@gib/eslint-config": "workspace:*", "@gib/eslint-config": "workspace:*",
"@gib/prettier-config": "workspace:*", "@gib/prettier-config": "workspace:*",
"@gib/tsconfig": "workspace:*", "@gib/tsconfig": "workspace:*",
"@gib/vitest-config": "workspace:*",
"@types/node": "catalog:", "@types/node": "catalog:",
"convex-test": "catalog:test",
"eslint": "catalog:", "eslint": "catalog:",
"prettier": "catalog:", "prettier": "catalog:",
"react-email": "5.2.10", "react-email": "5.2.10",
"typescript": "catalog:" "typescript": "catalog:",
"vitest": "catalog:test"
}, },
"prettier": "@gib/prettier-config" "prettier": "@gib/prettier-config"
} }
@@ -0,0 +1,13 @@
import { convexTest } from 'convex-test';
import { describe, expect, test } from 'vitest';
import schema from '../../convex/schema';
const modules = import.meta.glob('../../convex/**/*.*s');
describe('convex-test harness', () => {
test('boots and executes against the project schema', async () => {
const t = convexTest(schema, modules);
expect(await t.run(() => Promise.resolve(42))).toBe(42);
});
});
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "@gib/tsconfig/base.json",
"compilerOptions": { "lib": ["ES2022", "DOM"], "types": ["node"] },
"include": ["tests", "vitest.config.ts"],
"exclude": ["node_modules", "convex/_generated"]
}
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from 'vitest/config';
import { convexProject, nodeProject } from '@gib/vitest-config';
export default defineConfig({
test: {
projects: [
convexProject('unit', ['tests/unit/**/*.test.ts']),
convexProject('integration', ['tests/integration/**/*.test.ts']),
nodeProject('component', ['tests/component/**/*.test.{ts,tsx}']),
],
},
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+7
View File
@@ -12,6 +12,9 @@
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint --flag unstable_native_nodejs_ts_config", "lint": "eslint --flag unstable_native_nodejs_ts_config",
"typecheck": "tsc --noEmit --emitDeclarationOnly false", "typecheck": "tsc --noEmit --emitDeclarationOnly false",
"test:unit": "vitest run --project unit --passWithNoTests",
"test:integration": "vitest run --project integration --passWithNoTests",
"test:component": "NODE_ENV=test vitest run --project component",
"ui-add": "bunx --bun shadcn@latest add && prettier src --write --list-different" "ui-add": "bunx --bun shadcn@latest add && prettier src --write --list-different"
}, },
"dependencies": { "dependencies": {
@@ -53,11 +56,15 @@
"@gib/eslint-config": "workspace:*", "@gib/eslint-config": "workspace:*",
"@gib/prettier-config": "workspace:*", "@gib/prettier-config": "workspace:*",
"@gib/tsconfig": "workspace:*", "@gib/tsconfig": "workspace:*",
"@gib/vitest-config": "workspace:*",
"@testing-library/react": "catalog:test",
"@types/react": "catalog:react19", "@types/react": "catalog:react19",
"eslint": "catalog:", "eslint": "catalog:",
"jsdom": "catalog:test",
"prettier": "catalog:", "prettier": "catalog:",
"react": "catalog:react19", "react": "catalog:react19",
"typescript": "catalog:", "typescript": "catalog:",
"vitest": "catalog:test",
"zod": "catalog:" "zod": "catalog:"
}, },
"peerDependencies": { "peerDependencies": {
+1 -1
View File
@@ -21,7 +21,7 @@ const BasedProgress = ({
value = 0, value = 0,
...props ...props
}: BasedProgressProps) => { }: BasedProgressProps) => {
const [progress, setProgress] = React.useState<number>(value); const [progress, setProgress] = React.useState<number>(value ?? 0);
React.useEffect(() => { React.useEffect(() => {
const id = window.setInterval(() => { const id = window.setInterval(() => {
+44 -19
View File
@@ -105,6 +105,15 @@ ${colorConfig
const ChartTooltip = RechartsPrimitive.Tooltip; const ChartTooltip = RechartsPrimitive.Tooltip;
type ChartPayloadItem = {
name?: string | number;
value?: number | string;
dataKey?: string | number;
type?: string;
color?: string;
payload?: Record<string, unknown> & { fill?: string };
};
const ChartTooltipContent = ({ const ChartTooltipContent = ({
active, active,
payload, payload,
@@ -119,14 +128,29 @@ const ChartTooltipContent = ({
color, color,
nameKey, nameKey,
labelKey, labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> & }: React.ComponentProps<'div'> & {
React.ComponentProps<'div'> & { active?: boolean;
hideLabel?: boolean; payload?: ChartPayloadItem[];
hideIndicator?: boolean; label?: React.ReactNode;
indicator?: 'line' | 'dot' | 'dashed'; labelFormatter?: (
nameKey?: string; value: React.ReactNode,
labelKey?: string; payload: ChartPayloadItem[],
}) => { ) => React.ReactNode;
formatter?: (
value: number | string,
name: string | number,
item: ChartPayloadItem,
index: number,
itemPayload: ChartPayloadItem['payload'],
) => React.ReactNode;
color?: string;
labelClassName?: string;
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: 'line' | 'dot' | 'dashed';
nameKey?: string;
labelKey?: string;
}) => {
const { config } = useChart(); const { config } = useChart();
const tooltipLabel = React.useMemo(() => { const tooltipLabel = React.useMemo(() => {
@@ -139,7 +163,7 @@ const ChartTooltipContent = ({
const itemConfig = getPayloadConfigFromPayload(config, item, key); const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value = const value =
!labelKey && typeof label === 'string' !labelKey && typeof label === 'string'
? config[label]?.label ?? label ? (config[label]?.label ?? label)
: itemConfig?.label; : itemConfig?.label;
if (labelFormatter) { if (labelFormatter) {
@@ -185,17 +209,17 @@ const ChartTooltipContent = ({
.map((item, index) => { .map((item, index) => {
const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`; const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key); const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color ?? item.payload.fill ?? item.color; const indicatorColor = color ?? item.payload?.fill ?? item.color;
return ( return (
<div <div
key={item.dataKey} key={item.dataKey ?? index}
className={cn( className={cn(
'[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5', '[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
indicator === 'dot' && 'items-center', indicator === 'dot' && 'items-center',
)} )}
> >
{formatter && item?.value !== undefined && item.name ? ( {formatter && item.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload) formatter(item.value, item.name, item, index, item.payload)
) : ( ) : (
<> <>
@@ -259,11 +283,12 @@ const ChartLegendContent = ({
payload, payload,
verticalAlign = 'bottom', verticalAlign = 'bottom',
nameKey, nameKey,
}: React.ComponentProps<'div'> & }: React.ComponentProps<'div'> & {
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & { payload?: ChartPayloadItem[];
hideIcon?: boolean; verticalAlign?: 'top' | 'bottom';
nameKey?: string; hideIcon?: boolean;
}) => { nameKey?: string;
}) => {
const { config } = useChart(); const { config } = useChart();
if (!payload?.length) { if (!payload?.length) {
@@ -280,13 +305,13 @@ const ChartLegendContent = ({
> >
{payload {payload
.filter((item) => item.type !== 'none') .filter((item) => item.type !== 'none')
.map((item) => { .map((item, index) => {
const key = `${nameKey ?? item.dataKey ?? 'value'}`; const key = `${nameKey ?? item.dataKey ?? 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key); const itemConfig = getPayloadConfigFromPayload(config, item, key);
return ( return (
<div <div
key={item.value} key={item.value ?? index}
className={cn( className={cn(
'[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3', '[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3',
)} )}
+10 -7
View File
@@ -68,13 +68,16 @@ const ComboboxInput = ({
/> />
<InputGroupAddon align='inline-end'> <InputGroupAddon align='inline-end'>
{showTrigger && ( {showTrigger && (
<InputGroupButton <ComboboxTrigger
size='icon-xs' render={
variant='ghost' <InputGroupButton
render={<ComboboxTrigger />} size='icon-xs'
data-slot='input-group-button' variant='ghost'
className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent' data-slot='input-group-button'
disabled={disabled} className='group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent'
disabled={disabled}
/>
}
/> />
)} )}
{showClear && <ComboboxClear disabled={disabled} />} {showClear && <ComboboxClear disabled={disabled} />}
+1
View File
@@ -0,0 +1 @@
declare module '*.css';
+19 -15
View File
@@ -294,19 +294,17 @@ export const ImageCropApply = ({
if (asChild) { if (asChild) {
return ( return (
<Slot.Root onClick={handleClick} {...(props as any)}> <Slot.Root
onClick={handleClick}
{...(props as ComponentProps<typeof Slot.Root>)}
>
{children} {children}
</Slot.Root> </Slot.Root>
); );
} }
return ( return (
<Button <Button onClick={handleClick} size='icon' variant='ghost' {...props}>
onClick={handleClick}
size='icon'
variant='ghost'
{...(props as any)}
>
{children ?? <CropIcon className='size-4' />} {children ?? <CropIcon className='size-4' />}
</Button> </Button>
); );
@@ -331,19 +329,17 @@ export const ImageCropReset = ({
if (asChild) { if (asChild) {
return ( return (
<Slot.Root onClick={handleClick} {...(props as any)}> <Slot.Root
onClick={handleClick}
{...(props as ComponentProps<typeof Slot.Root>)}
>
{children} {children}
</Slot.Root> </Slot.Root>
); );
} }
return ( return (
<Button <Button onClick={handleClick} size='icon' variant='ghost' {...props}>
onClick={handleClick}
size='icon'
variant='ghost'
{...(props as any)}
>
{children ?? <RotateCcwIcon className='size-4' />} {children ?? <RotateCcwIcon className='size-4' />}
</Button> </Button>
); );
@@ -373,7 +369,15 @@ export const Cropper = ({
onChange={onChange} onChange={onChange}
onComplete={onComplete} onComplete={onComplete}
onCrop={onCrop} onCrop={onCrop}
{...(props as any)} {...(props as Omit<
ImageCropProps,
| 'file'
| 'maxImageSize'
| 'onChange'
| 'onComplete'
| 'onCrop'
| 'children'
>)}
> >
<ImageCropContent className={className} style={style} /> <ImageCropContent className={className} style={style} />
</ImageCrop> </ImageCrop>
@@ -0,0 +1,13 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Button } from '../../src/button';
describe('Button', () => {
it('renders its children', () => {
render(<Button>Click me</Button>);
expect(
screen.getByRole('button', { name: 'Click me' }),
).toBeInTheDocument();
});
});
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';
+1 -1
View File
@@ -5,6 +5,6 @@
"jsx": "preserve", "jsx": "preserve",
"rootDir": "." "rootDir": "."
}, },
"include": ["src"], "include": ["src", "tests", "vitest.config.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from 'vitest/config';
import { jsdomProject, nodeProject } from '@gib/vitest-config';
export default defineConfig({
test: {
projects: [
nodeProject('unit', ['tests/unit/**/*.test.{ts,tsx}']),
nodeProject('integration', ['tests/integration/**/*.test.{ts,tsx}']),
jsdomProject('component', ['tests/component/**/*.test.{ts,tsx}']),
],
},
});
+9 -16
View File
@@ -1,19 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" ENVIRONMENT="${1:-staging}"
[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: build-next-app [dev|staging]" >&2; exit 2; }
ENV_FILE="$ROOT_DIR/.env" ENV_FILE="${CI_ENV_FILE:-}"
COMPOSE_FILE="$ROOT_DIR/docker/compose.yml" cleanup() { [[ -n "$ENV_FILE" && "$ENV_FILE" != "${CI_ENV_FILE:-}" ]] && rm -f "$ENV_FILE" || true; }
trap cleanup EXIT
if [ ! -f "$ENV_FILE" ]; then if [[ -z "$ENV_FILE" && -z "${CI:-}" ]]; then ENV_FILE="$(mktemp)"; sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"; fi
echo "Error: env file not found at $ENV_FILE" >&2 args=(); [[ -z "$ENV_FILE" ]] || args+=(--env-file "$ENV_FILE")
exit 1 docker compose "${args[@]}" -f "$ROOT_DIR/docker/compose.yml" build convexmonorepo-next
fi
set -a
source "$ENV_FILE"
set +a
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" build "$NEXT_CONTAINER_NAME"
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
COMPOSE_FILE="$ROOT_DIR/docker/compose.local.yml"
STATE_FILE="$ROOT_DIR/.local/dev.generated.env"
PAYLOAD_SEED_MARKER="$ROOT_DIR/.local/payload-seed-state.env"
WIPE=false
[ "${1:-}" = "--wipe" ] && WIPE=true
if command -v docker >/dev/null 2>&1; then RUNTIME=docker
elif command -v podman >/dev/null 2>&1; then RUNTIME=podman
else echo "Docker or Podman not found; nothing to stop." >&2; exit 0; fi
ENV_FILE="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-local.XXXXXX.env")"
trap 'rm -f "$ENV_FILE"' EXIT
sh "$ROOT_DIR/scripts/export-env" dev > "$ENV_FILE"
if [ "$WIPE" = true ]; then
"$RUNTIME" compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" down -v
if [ -f "$STATE_FILE" ]; then
tmp="${STATE_FILE}.tmp"
grep -v '^CONVEX_SELF_HOSTED_ADMIN_KEY=' "$STATE_FILE" > "$tmp" || true
mv "$tmp" "$STATE_FILE"
fi
rm -f "$PAYLOAD_SEED_MARKER"
echo "Local stack and both data volumes removed; generated admin key and Payload seed marker cleared."
else
"$RUNTIME" compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" down
echo "Local stack stopped; Payload and Convex data preserved."
fi
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
# Restore the local Payload database from the local snapshot only when the
# database has not already been seeded. Does not contact staging/production.
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
COMPOSE_FILE="$ROOT_DIR/docker/compose.local.yml"
SNAPSHOT="$ROOT_DIR/.local/payload-staging.dump"
MARKER="$ROOT_DIR/.local/payload-seed-state.env"
DEV_ENV=""
FORCE=false
ASSUME_YES=false
for arg in "$@"; do
case "$arg" in
--force) FORCE=true ;;
--yes) ASSUME_YES=true ;;
*) printf 'usage: seed-payload [--force] [--yes]\n' >&2; exit 2 ;;
esac
done
info() { printf '▶ %s\n' "$*"; }
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
if command -v docker >/dev/null 2>&1; then RUNTIME=docker
elif command -v podman >/dev/null 2>&1; then RUNTIME=podman
else die "Docker or Podman is required."; fi
"$RUNTIME" info >/dev/null 2>&1 || die "$RUNTIME is not usable."
cleanup() { [ -z "$DEV_ENV" ] || rm -f "$DEV_ENV"; }
trap cleanup EXIT INT TERM HUP
if [ ! -s "$SNAPSHOT" ]; then
echo "No local Payload snapshot found at .local/payload-staging.dump; skipping Payload seed."
exit 0
fi
mkdir -p "$ROOT_DIR/.local"
chmod 700 "$ROOT_DIR/.local"
next_is_running=false
if command -v ss >/dev/null 2>&1; then
ss -ltnH 'sport = :3000' | grep -q . && next_is_running=true
elif command -v lsof >/dev/null 2>&1; then
lsof -nP -iTCP:3000 -sTCP:LISTEN >/dev/null 2>&1 && next_is_running=true
elif curl -sS --max-time 1 http://localhost:3000 >/dev/null 2>&1; then
next_is_running=true
fi
if [ "$next_is_running" = true ]; then
die "Next is running on port 3000. Stop bun dev:next before replacing Payload data."
fi
DEV_ENV="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-dev.XXXXXX.env")"
sh "$ROOT_DIR/scripts/export-env" dev > "$DEV_ENV"
bunx dotenv -e "$DEV_ENV" -- node -e '
const raw = process.env.PAYLOAD_DB_URL;
if (!raw) process.exit(2);
const url = new URL(raw);
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
if (!local) process.exit(3);
' || die "Refusing to seed: dev PAYLOAD_DB_URL is not localhost."
dc() { "$RUNTIME" compose --env-file "$DEV_ENV" -f "$COMPOSE_FILE" "$@"; }
POSTGRES_USER="$(bunx dotenv -e "$DEV_ENV" -- sh -c 'printf %s "$POSTGRES_USER"')"
POSTGRES_DB="$(bunx dotenv -e "$DEV_ENV" -- sh -c 'printf %s "$POSTGRES_DB"')"
[ -n "$POSTGRES_USER" ] && [ -n "$POSTGRES_DB" ] || die "Local Postgres configuration is incomplete."
info "Ensuring local Payload Postgres is running"
dc up -d postgres >/dev/null
for i in $(seq 1 30); do
if dc exec -T postgres pg_isready -U "$POSTGRES_USER" >/dev/null 2>&1; then break; fi
[ "$i" -lt 30 ] || die "Local Payload Postgres did not become ready."
sleep 1
done
SNAPSHOT_HASH="$(sha256sum "$SNAPSHOT" | awk '{print $1}')"
marker_exists=false
# Earlier versions stored this marker in the Payload database. Payload/Drizzle
# treats unmanaged tables as drift and prompts to delete them during dev startup,
# so keep seed state in .local/ and remove the legacy table from local DBs.
dc exec -T postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 \
-c 'DROP TABLE IF EXISTS _local_seed_state' >/dev/null
if [ -s "$MARKER" ] && grep -qx "PAYLOAD_SNAPSHOT_SHA256=$SNAPSHOT_HASH" "$MARKER"; then
marker_exists=true
fi
if [ "$marker_exists" = true ] && [ "$FORCE" != true ]; then
echo "Payload snapshot seed already applied; skipping. Use --force to restore again."
exit 0
fi
if [ "$FORCE" = true ] && [ "$ASSUME_YES" != true ]; then
printf 'This will replace the LOCAL Payload database from .local/payload-staging.dump.\n'
read -r -p 'Continue? [y/N] ' answer
case "$answer" in y|Y|yes|YES) ;; *) echo "Cancelled."; exit 0 ;; esac
fi
info "Restoring local Payload database from .local/payload-staging.dump"
dc exec -T postgres dropdb --force --if-exists -U "$POSTGRES_USER" "$POSTGRES_DB"
dc exec -T postgres createdb -U "$POSTGRES_USER" -O "$POSTGRES_USER" "$POSTGRES_DB"
dc exec -T postgres pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
--no-owner --no-acl --exit-on-error < "$SNAPSHOT"
{
printf 'PAYLOAD_SNAPSHOT_SHA256=%s\n' "$SNAPSHOT_HASH"
printf 'PAYLOAD_SNAPSHOT_SEEDED_AT=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > "$MARKER"
chmod 600 "$MARKER"
echo "Payload snapshot seed applied."
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Refresh the local Payload seed snapshot from staging, then force-apply it to
# the local development Payload database. Normal db:up never calls staging.
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
COMPOSE_FILE="$ROOT_DIR/docker/compose.local.yml"
SNAPSHOT="$ROOT_DIR/.local/payload-staging.dump"
STAGING_ENV=""
DEV_ENV=""
SOURCE_PG_ENV=""
ASSUME_YES=false
[ "${1:-}" = "--yes" ] && ASSUME_YES=true
info() { printf '▶ %s\n' "$*"; }
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
if command -v docker >/dev/null 2>&1; then RUNTIME=docker
elif command -v podman >/dev/null 2>&1; then RUNTIME=podman
else die "Docker or Podman is required."; fi
"$RUNTIME" info >/dev/null 2>&1 || die "$RUNTIME is not usable."
mkdir -p "$ROOT_DIR/.local"
chmod 700 "$ROOT_DIR/.local"
cleanup() {
[ -z "$STAGING_ENV" ] || rm -f "$STAGING_ENV"
[ -z "$DEV_ENV" ] || rm -f "$DEV_ENV"
[ -z "$SOURCE_PG_ENV" ] || rm -f "$SOURCE_PG_ENV"
rm -f "${SNAPSHOT}.tmp"
}
trap cleanup EXIT INT TERM HUP
STAGING_ENV="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-staging.XXXXXX.env")"
DEV_ENV="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-dev.XXXXXX.env")"
sh "$ROOT_DIR/scripts/export-env" staging > "$STAGING_ENV"
sh "$ROOT_DIR/scripts/export-env" dev > "$DEV_ENV"
validate_url() {
local env_file="$1" expected="$2"
bunx dotenv -e "$env_file" -- node -e '
const expected = process.argv[1];
const raw = process.env.PAYLOAD_DB_URL;
if (!raw) process.exit(2);
const url = new URL(raw);
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
if ((expected === "local") !== local) process.exit(3);
' "$expected" || die "PAYLOAD_DB_URL safety check failed for the $expected environment."
}
validate_url "$STAGING_ENV" remote
validate_url "$DEV_ENV" local
next_is_running=false
if command -v ss >/dev/null 2>&1; then
ss -ltnH 'sport = :3000' | grep -q . && next_is_running=true
elif command -v lsof >/dev/null 2>&1; then
lsof -nP -iTCP:3000 -sTCP:LISTEN >/dev/null 2>&1 && next_is_running=true
elif curl -sS --max-time 1 http://localhost:3000 >/dev/null 2>&1; then
next_is_running=true
fi
if [ "$next_is_running" = true ]; then
die "Next is running on port 3000. Stop bun dev:next before replacing Payload data."
fi
SOURCE_PG_ENV="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-pg.XXXXXX.env")"
PG_ENV_FILE="$SOURCE_PG_ENV" bunx dotenv -e "$STAGING_ENV" -- sh -c '
umask 077
printf "PGDATABASE=%s\n" "$PAYLOAD_DB_URL" > "$PG_ENV_FILE"
'
if [ "$ASSUME_YES" != true ]; then
printf 'This will download staging Payload data and replace the LOCAL Payload database.\n'
printf 'The snapshot may contain user records and password hashes; it stays under .local/.\n'
read -r -p 'Continue? [y/N] ' answer
case "$answer" in y|Y|yes|YES) ;; *) echo "Cancelled."; exit 0 ;; esac
fi
info "Exporting a read-only snapshot from staging Payload Postgres"
"$RUNTIME" run --rm --network host --env-file "$SOURCE_PG_ENV" \
docker.io/library/postgres:17 \
sh -c 'exec pg_dump --dbname="$PGDATABASE" --format=custom --no-owner --no-acl' \
> "${SNAPSHOT}.tmp"
mv "${SNAPSHOT}.tmp" "$SNAPSHOT"
chmod 600 "$SNAPSHOT"
info "Ensuring local Payload Postgres is running"
bash "$ROOT_DIR/scripts/db/seed-payload" --force --yes
printf '\nLocal Payload seed snapshot refreshed and applied.\n'
printf 'Snapshot: .local/payload-staging.dump\n'
printf 'Normal db:up will reuse this snapshot and skip after .local/payload-seed-state.env exists.\n'
Executable
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
COMPOSE_FILE="$ROOT_DIR/docker/compose.local.yml"
STATE_FILE="$ROOT_DIR/.local/dev.generated.env"
ENV_FILE=""
info() { printf '▶ %s\n' "$*"; }
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
if command -v docker >/dev/null 2>&1; then RUNTIME=docker
elif command -v podman >/dev/null 2>&1; then RUNTIME=podman
else die "Docker or Podman is required."; fi
"$RUNTIME" info >/dev/null 2>&1 || die "$RUNTIME is not usable."
mkdir -p "$ROOT_DIR/.local"
cleanup() { [ -z "$ENV_FILE" ] || rm -f "$ENV_FILE"; }
trap cleanup EXIT
refresh_env() {
local next
next="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-local.XXXXXX.env")"
sh "$ROOT_DIR/scripts/export-env" dev > "$next" || { rm -f "$next"; die "Unable to export Infisical dev."; }
[ -z "$ENV_FILE" ] || rm -f "$ENV_FILE"
ENV_FILE="$next"
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
}
dc() { "$RUNTIME" compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
upsert_state() {
local key="$1" value="$2" tmp escaped
tmp="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-state.XXXXXX.env")"
[ ! -f "$STATE_FILE" ] || grep -v "^${key}=" "$STATE_FILE" > "$tmp" || true
escaped="$(printf '%s' "$value" | sed "s/'/'\\\\''/g")"
printf "%s='%s'\n" "$key" "$escaped" >> "$tmp"
mv "$tmp" "$STATE_FILE"
}
refresh_env
info "Starting local Payload Postgres, Convex, and dashboard"
dc up -d
info "Waiting for Payload Postgres"
for i in $(seq 1 30); do
dc exec -T postgres pg_isready -U "${POSTGRES_USER:-convexmonorepo}" >/dev/null 2>&1 && break
[ "$i" -lt 30 ] || die "Postgres did not become ready."
sleep 1
done
info "Waiting for Convex at http://localhost:${BACKEND_PORT:-3210}"
for i in $(seq 1 60); do
curl -fs "http://localhost:${BACKEND_PORT:-3210}/version" >/dev/null 2>&1 && break
[ "$i" -lt 60 ] || die "Convex did not become ready."
sleep 2
done
if [ -z "${CONVEX_SELF_HOSTED_ADMIN_KEY:-}" ]; then
admin_key="$(dc exec -T convex-backend ./generate_admin_key.sh 2>/dev/null | grep -E '.+\|.+' | tail -n1 | tr -d '\r')"
[ -n "$admin_key" ] || die "Unable to generate the Convex admin key."
upsert_state CONVEX_SELF_HOSTED_ADMIN_KEY "$admin_key"
refresh_env
info "Generated the machine-local Convex admin key"
fi
info "Deploying Convex schema and functions"
(cd "$ROOT_DIR/packages/backend" && bun run setup)
convex_env_names="$(
sh "$ROOT_DIR/scripts/with-env" dev -- bash -c \
'cd packages/backend && bunx convex env list' 2>/dev/null \
| sed -n 's/^\([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p'
)"
if ! printf '%s\n' "$convex_env_names" | grep -qx 'JWT_PRIVATE_KEY' \
|| ! printf '%s\n' "$convex_env_names" | grep -qx 'JWKS' \
|| ! printf '%s\n' "$convex_env_names" | grep -qx 'SITE_URL'; then
info "Configuring local Convex Auth keys"
auth_keys="$(node "$ROOT_DIR/scripts/generate-convex-auth-keys.mjs")"
jwt="$(printf '%s\n' "$auth_keys" | sed -n 's/^JWT_PRIVATE_KEY="\(.*\)"$/\1/p')"
jwks="$(printf '%s\n' "$auth_keys" | sed -n 's/^JWKS=//p')"
JWT_VAL="$jwt" JWKS_VAL="$jwks" sh "$ROOT_DIR/scripts/with-env" dev -- bash -c '
cd packages/backend
bunx convex env set "JWT_PRIVATE_KEY=$JWT_VAL" >/dev/null
bunx convex env set "JWKS=$JWKS_VAL" >/dev/null
bunx convex env set "SITE_URL=http://localhost:3000" >/dev/null
'
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}"
+18 -40
View File
@@ -1,49 +1,27 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
ENV_FILE="$ROOT_DIR/.env"
COMPOSE_FILE="$ROOT_DIR/docker/compose.yml" COMPOSE_FILE="$ROOT_DIR/docker/compose.yml"
ENVIRONMENT="${1:-staging}"
if [[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]]; then shift || true; else ENVIRONMENT=staging; fi
if [ ! -f "$ENV_FILE" ]; then ENV_FILE="${CI_ENV_FILE:-}"
echo "Error: env file not found at $ENV_FILE" >&2 cleanup() { [[ -n "$ENV_FILE" && "$ENV_FILE" != "${CI_ENV_FILE:-}" ]] && rm -f "$ENV_FILE" || true; }
exit 1 trap cleanup EXIT
if [[ -z "$ENV_FILE" && -z "${CI:-}" ]]; then
ENV_FILE="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-compose.XXXXXX.env")"
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
fi fi
if [ ! -f "$COMPOSE_FILE" ]; then args=()
echo "Error: compose file not found at $COMPOSE_FILE" >&2 [[ -z "$ENV_FILE" ]] || args+=(--env-file "$ENV_FILE")
exit 1 translated=()
fi
set -a
source "$ENV_FILE"
set +a
translated_args=()
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in backend) translated+=(convexmonorepo-backend) ;; dashboard) translated+=(convexmonorepo-dashboard) ;; next) translated+=(convexmonorepo-next) ;; *) translated+=("$arg") ;; esac
backend)
: "${BACKEND_CONTAINER_NAME:?BACKEND_CONTAINER_NAME is not set in .env}"
translated_args+=("$BACKEND_CONTAINER_NAME")
;;
dashboard)
: "${DASHBOARD_CONTAINER_NAME:?DASHBOARD_CONTAINER_NAME is not set in .env}"
translated_args+=("$DASHBOARD_CONTAINER_NAME")
;;
next)
: "${NEXT_CONTAINER_NAME:?NEXT_CONTAINER_NAME is not set in .env}"
translated_args+=("$NEXT_CONTAINER_NAME")
;;
*)
translated_args+=("$arg")
;;
esac
done done
set +e
exec docker compose \ docker compose "${args[@]}" -f "$COMPOSE_FILE" "${translated[@]}"
--env-file "$ENV_FILE" \ status=$?
-f "$COMPOSE_FILE" \ set -e
"${translated_args[@]}" exit "$status"
Executable
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
if [ -n "${CI:-}" ]; then echo "CI detected; skipping local e2e."; exit 0; fi
if [ -n "${SKIP_E2E:-}" ]; then echo "SKIP_E2E set; skipping local e2e."; exit 0; fi
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
bash "$ROOT_DIR/scripts/db/up"
echo "Local-stack smoke checks passed; no seeded browser flow is configured."
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env sh
set -eu
[ "$#" -eq 1 ] || { echo "usage: export-env <dev|staging>" >&2; exit 2; }
ENVIRONMENT="$1"
case "$ENVIRONMENT" in dev|staging) ;; *) echo "export-env: expected dev or staging" >&2; exit 2 ;; esac
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
STATE_FILE="$ROOT_DIR/.local/$ENVIRONMENT.generated.env"
if [ -n "${CI:-}" ]; then
echo "export-env: refusing to export secrets in CI; use injected variables or CI_ENV_FILE." >&2
exit 1
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; }
(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
}
if [ -f "$STATE_FILE" ]; then
printf '\n'
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in ''|'#'*) printf '%s\n' "$line"; continue ;; esac
key=${line%%=*}
value=${line#*=}
case "$value" in \'*\') value=${value#\'}; value=${value%\'} ;; \"*\") value=${value#\"}; value=${value%\"} ;; esac
escaped=$(printf '%s' "$value" | sed "s/'/'\\\\''/g")
printf "%s='%s'\n" "$key" "$escaped"
done < "$STATE_FILE"
fi
+6 -18
View File
@@ -1,20 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ENVIRONMENT="${1:-staging}"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" [[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: generate-convex-admin-key [dev|staging]" >&2; exit 2; }
ENV_FILE="$(mktemp)"; trap 'rm -f "$ENV_FILE"' EXIT
ENV_FILE="$ROOT_DIR/.env" sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
COMPOSE_FILE="$ROOT_DIR/docker/compose.yml" docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" exec convexmonorepo-backend ./generate_admin_key.sh
if [ ! -f "$ENV_FILE" ]; then
echo "Error: env file not found at $ENV_FILE" >&2
exit 1
fi
set -a
source "$ENV_FILE"
set +a
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec \
"$BACKEND_CONTAINER_NAME" ./generate_admin_key.sh
+7 -20
View File
@@ -1,22 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ENVIRONMENT="${1:-staging}"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" [[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: update-convex [dev|staging]" >&2; exit 2; }
ENV_FILE="$(mktemp)"; trap 'rm -f "$ENV_FILE"' EXIT
ENV_FILE="$ROOT_DIR/.env" sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
COMPOSE_FILE="$ROOT_DIR/docker/compose.yml" docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" pull convexmonorepo-backend convexmonorepo-dashboard
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" up -d convexmonorepo-backend convexmonorepo-dashboard
if [ ! -f "$ENV_FILE" ]; then
echo "Error: env file not found at $ENV_FILE" >&2
exit 1
fi
set -a
source "$ENV_FILE"
set +a
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" pull \
"$BACKEND_CONTAINER_NAME" "$DASHBOARD_CONTAINER_NAME"
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d \
"$BACKEND_CONTAINER_NAME" "$DASHBOARD_CONTAINER_NAME"
+7 -18
View File
@@ -1,20 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ENVIRONMENT="${1:-staging}"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" [[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: update-next-app [dev|staging]" >&2; exit 2; }
ENV_FILE="$(mktemp)"; trap 'rm -f "$ENV_FILE"' EXIT
ENV_FILE="$ROOT_DIR/.env" sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
COMPOSE_FILE="$ROOT_DIR/docker/compose.yml" docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" build convexmonorepo-next
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" up -d convexmonorepo-next
if [ ! -f "$ENV_FILE" ]; then
echo "Error: env file not found at $ENV_FILE" >&2
exit 1
fi
set -a
source "$ENV_FILE"
set +a
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" build "$NEXT_CONTAINER_NAME"
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d "$NEXT_CONTAINER_NAME"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env sh
set -eu
if [ "$#" -lt 1 ]; then
echo "usage: with-env <dev|staging> -- <command> [args...]" >&2
exit 2
fi
ENVIRONMENT="$1"
shift
[ "${1:-}" = "--" ] && shift
[ "$#" -gt 0 ] || { echo "with-env: no command given" >&2; exit 2; }
case "$ENVIRONMENT" in dev|staging) ;; *) echo "with-env: expected dev or staging" >&2; exit 2 ;; esac
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
STATE_FILE="$ROOT_DIR/.local/$ENVIRONMENT.generated.env"
if [ -n "${CI:-}" ]; then
export WITH_ENV_SOURCE=ci WITH_ENV_ENVIRONMENT="$ENVIRONMENT" WITH_ENV_STATE_FILE="$STATE_FILE"
exec "$@"
fi
command -v infisical >/dev/null 2>&1 || {
echo "with-env: install Infisical, run 'infisical login', and link this repo with 'infisical init'." >&2
exit 1
}
[ -f "$ROOT_DIR/.infisical.json" ] || { echo "with-env: .infisical.json is missing." >&2; exit 1; }
TMP_ENV="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-$ENVIRONMENT.XXXXXX.env")"
trap 'rm -f "$TMP_ENV"' EXIT INT TERM HUP
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$TMP_ENV"
export WITH_ENV_SOURCE=infisical WITH_ENV_ENVIRONMENT="$ENVIRONMENT" WITH_ENV_STATE_FILE="$STATE_FILE"
set +e
bunx dotenv -e "$TMP_ENV" -- "$@"
status=$?
set -e
exit "$status"
-1
View File
@@ -1 +0,0 @@
[["1","2","3","4","5","6"],{"key":"7","value":"8"},{"key":"9","value":"10"},{"key":"11","value":"12"},{"key":"13","value":"14"},{"key":"15","value":"16"},{"key":"17","value":"18"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/base.ts",{"size":2963,"mtime":1774546669459,"hash":"19","data":"20"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/nextjs.ts",{"size":440,"mtime":1768155639000,"hash":"21","data":"22"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/.cache/.prettiercache",{"size":1278,"mtime":1774718093280},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/package.json",{"size":1034,"mtime":1774588268325,"hash":"23","data":"24"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/react.ts",{"size":592,"mtime":1768155639000,"hash":"25","data":"26"},"/home/gib/Documents/Code/convex-monorepo/tools/eslint/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"27","data":"28"},"6a779439826cf31b5561a21273d134a9",{"hashOfOptions":"29"},"25c52c46972131dcc296288599ff108d",{"hashOfOptions":"30"},"a5326aca75246da261fd2e354257b45a",{"hashOfOptions":"31"},"2292935ede6baf909f6a0c61486e15da",{"hashOfOptions":"32"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"33"},"1686097143","2347540204","302976953","3406150487","1582266352"]
+23 -1
View File
@@ -11,6 +11,12 @@ import tseslint from 'typescript-eslint';
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const preferArrowPlugin = preferArrowFunctions as any; const preferArrowPlugin = preferArrowFunctions as any;
const turboRecommendedRules = (
turboPlugin as unknown as {
configs: { recommended: { rules: Record<string, string> } };
}
).configs.recommended.rules;
/** /**
* All packages that leverage t3-env should use this rule * All packages that leverage t3-env should use this rule
*/ */
@@ -59,7 +65,7 @@ export const baseConfig = defineConfig(
...tseslint.configs.stylisticTypeChecked, ...tseslint.configs.stylisticTypeChecked,
], ],
rules: { rules: {
...turboPlugin.configs.recommended.rules, ...turboRecommendedRules,
'@typescript-eslint/no-unused-vars': [ '@typescript-eslint/no-unused-vars': [
'warn', 'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
@@ -88,6 +94,22 @@ export const baseConfig = defineConfig(
], ],
}, },
}, },
{
files: [
'**/tests/**/*.{ts,tsx}',
'**/*.test.{ts,tsx}',
'**/*.spec.{ts,tsx}',
],
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unnecessary-condition': 'off',
},
},
{ {
linterOptions: { reportUnusedDisableDirectives: true }, linterOptions: { reportUnusedDisableDirectives: true },
languageOptions: { languageOptions: {
+7 -4
View File
@@ -1,15 +1,18 @@
import type { Linter } from 'eslint';
import reactPlugin from 'eslint-plugin-react'; import reactPlugin from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks'; import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config'; import { defineConfig } from 'eslint/config';
const reactFlat = reactPlugin.configs.flat as Record<string, Linter.Config>;
export const reactConfig = defineConfig( export const reactConfig = defineConfig(
{ {
files: ['**/*.ts', '**/*.tsx'], files: ['**/*.ts', '**/*.tsx'],
...reactPlugin.configs.flat.recommended, ...reactFlat.recommended,
...reactPlugin.configs.flat['jsx-runtime'], ...reactFlat['jsx-runtime'],
languageOptions: { languageOptions: {
...reactPlugin.configs.flat.recommended?.languageOptions, ...reactFlat.recommended?.languageOptions,
...reactPlugin.configs.flat['jsx-runtime']?.languageOptions, ...reactFlat['jsx-runtime']?.languageOptions,
globals: { globals: {
React: 'writable', React: 'writable',
}, },
+5
View File
@@ -1,5 +1,10 @@
{ {
"extends": "@gib/tsconfig/base.json", "extends": "@gib/tsconfig/base.json",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["node"]
},
"include": ["."], "include": ["."],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }
-1
View File
@@ -1 +0,0 @@
[["1","2","3","4"],{"key":"5","value":"6"},{"key":"7","value":"8"},{"key":"9","value":"10"},{"key":"11","value":"12"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/.cache/.prettiercache",{"size":832,"mtime":1774718093318},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/index.js",{"size":1194,"mtime":1768372320442,"hash":"13","data":"14"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/package.json",{"size":607,"mtime":1774032385569,"hash":"15","data":"16"},"/home/gib/Documents/Code/convex-monorepo/tools/prettier/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"17","data":"18"},"ecbaa91166a940dfcec8117059f52402",{"hashOfOptions":"19"},"11b634ce56ac720ac9a2860d77fbd2cc",{"hashOfOptions":"20"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"21"},"1828250668","802511607","4250532914"]
-1
View File
@@ -1 +0,0 @@
[{"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/postcss-config.js":"1"},{"size":70,"mtime":1768155639000,"results":"2","hashOfConfig":"3"},{"filePath":"4","messages":"5","suppressedMessages":"6","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"zvlfqu","/home/gib/Documents/Code/convex-monorepo/tools/tailwind/postcss-config.js",[],[]]
-1
View File
@@ -1 +0,0 @@
[["1","2","3","4","5","6","7"],{"key":"8","value":"9"},{"key":"10","value":"11"},{"key":"12","value":"13"},{"key":"14","value":"15"},{"key":"16","value":"17"},{"key":"18","value":"19"},{"key":"20","value":"21"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/.cache/.eslintcache",{"size":396,"mtime":1774717490012},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/.cache/.prettiercache",{"size":1450,"mtime":1774718093720},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/package.json",{"size":851,"mtime":1774032407411,"hash":"22","data":"23"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/tsconfig.json",{"size":94,"mtime":1766222924000,"hash":"24","data":"25"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/eslint.config.ts",{"size":143,"mtime":1768155639000,"hash":"26","data":"27"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/theme.css",{"size":7273,"mtime":1768320378000,"hash":"28","data":"29"},"/home/gib/Documents/Code/convex-monorepo/tools/tailwind/postcss-config.js",{"size":70,"mtime":1768155639000,"hash":"30","data":"31"},"0d22e47f57739db9de04c6f8420d6fb5",{"hashOfOptions":"32"},"b3c77d33a30318d89c9c2cafcbe00bbe",{"hashOfOptions":"33"},"b8fec960cb32340eea62ca1485093e68",{"hashOfOptions":"34"},"e40c2569ef375a9c828c80c0f9ce1bf2",{"hashOfOptions":"35"},"9a944fbda06979be39571bd9bd00b0d9",{"hashOfOptions":"36"},"286235122","2846522359","1288936688","2683656544","1694755693"]
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"extends": "@gib/tsconfig/base.json", "extends": "@gib/tsconfig/base.json",
"include": ["."], "include": ["."],
"exclude": ["node_modules"] "exclude": ["node_modules", "eslint.config.ts"]
} }
+24
View File
@@ -0,0 +1,24 @@
import { fileURLToPath } from 'node:url';
import react from '@vitejs/plugin-react';
import { defineProject } from 'vitest/config';
const jsdomSetup = fileURLToPath(new URL('./setup-jsdom.ts', import.meta.url));
export const nodeProject = (name: string, include: string[]) =>
defineProject({ test: { name, environment: 'node', include } });
export const jsdomProject = (name: string, include: string[]) =>
defineProject({
plugins: [react()],
test: { name, environment: 'jsdom', include, setupFiles: [jsdomSetup] },
});
export const convexProject = (name: string, include: string[]) =>
defineProject({
test: {
name,
environment: 'edge-runtime',
include,
server: { deps: { inline: ['convex-test'] } },
},
});
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@gib/vitest-config",
"private": true,
"type": "module",
"exports": {
".": "./index.ts",
"./setup-jsdom": "./setup-jsdom.ts"
},
"scripts": {
"clean": "git clean -xdf .cache .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@testing-library/jest-dom": "catalog:test",
"@vitejs/plugin-react": "catalog:test"
},
"devDependencies": {
"@gib/prettier-config": "workspace:*",
"@gib/tsconfig": "workspace:*",
"@types/node": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:test"
},
"prettier": "@gib/prettier-config"
}
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "@gib/tsconfig/base.json",
"compilerOptions": { "lib": ["ES2022", "dom", "dom.iterable"] },
"include": ["."],
"exclude": ["node_modules"]
}
+33 -2
View File
@@ -1,9 +1,11 @@
{ {
"$schema": "https://v2-8-20.turborepo.dev/schema.json", "$schema": "https://v2-9-18.turborepo.dev/schema.json",
"globalDependencies": ["**/.env.*local"], "globalDependencies": [".infisical.json", ".local/*.env"],
"globalEnv": [ "globalEnv": [
"NODE_ENV", "NODE_ENV",
"INFISICAL_ENV",
"SENTRY_AUTH_TOKEN", "SENTRY_AUTH_TOKEN",
"SENTRY_DISABLE_AUTO_UPLOAD",
"PAYLOAD_SECRET", "PAYLOAD_SECRET",
"PAYLOAD_DB_URL", "PAYLOAD_DB_URL",
"NEXT_PUBLIC_SITE_URL", "NEXT_PUBLIC_SITE_URL",
@@ -22,6 +24,32 @@
"AUTH_AUTHENTIK_ID", "AUTH_AUTHENTIK_ID",
"AUTH_AUTHENTIK_SECRET", "AUTH_AUTHENTIK_SECRET",
"AUTH_AUTHENTIK_ISSUER", "AUTH_AUTHENTIK_ISSUER",
"SKIP_E2E",
"BASE_URL",
"NETWORK",
"NEXT_CONTAINER_NAME",
"NEXT_DOMAIN",
"NEXT_PORT",
"BACKEND_TAG",
"DASHBOARD_TAG",
"BACKEND_CONTAINER_NAME",
"DASHBOARD_CONTAINER_NAME",
"BACKEND_DOMAIN",
"DASHBOARD_DOMAIN",
"INSTANCE_NAME",
"INSTANCE_SECRET",
"CONVEX_CLOUD_ORIGIN",
"CONVEX_SITE_ORIGIN",
"NEXT_PUBLIC_DEPLOYMENT_URL",
"LOCAL_INSTANCE_NAME",
"LOCAL_INSTANCE_SECRET",
"POSTGRES_USER",
"POSTGRES_PASSWORD",
"POSTGRES_DB",
"POSTGRES_PORT",
"BACKEND_PORT",
"SITE_PROXY_PORT",
"DASHBOARD_PORT"
], ],
"globalPassThroughEnv": ["NODE_ENV"], "globalPassThroughEnv": ["NODE_ENV"],
"ui": "tui", "ui": "tui",
@@ -57,6 +85,9 @@
"dependsOn": ["^topo", "^build"], "dependsOn": ["^topo", "^build"],
"outputs": [".cache/tsbuildinfo.json"] "outputs": [".cache/tsbuildinfo.json"]
}, },
"test:unit": { "dependsOn": ["^build"], "cache": false },
"test:integration": { "dependsOn": ["^build"], "cache": false },
"test:component": { "dependsOn": ["^build"], "cache": false },
"clean": { "clean": {
"cache": false "cache": false
}, },