Hi π This is an overengineered way to show past projects / experiment with new tech. It's a lightweight, mobile-friendly React app in Typescript, powered by Next and hosted on Vercel. It's a monorepo using pnpm workspaces and Turbo.
turbo devstart dev server (see Dev workflow below)turbo buildprod build (CI)turbo build:serveprod build + serve (local testing)turbo build:analyzebundle size analysisturbo checkBiome lint + formatturbo check:typestypecheckturbo testunit tests (Jest)turbo graphql:schemarefresh Contentful GraphQL schema for editor toolingturbo migraterun migrationsturbo migrate:generate -- --name <name>create migrationturbo migrate:statusshow migration statusturbo migrate:undorollback last migrationturbo cleanclean caches, generated files, and.envturbo topo --graph=graph.htmlmonorepo dependency graph
turbo dev runs these steps in order before starting the Next.js dev server:
**dg#env**- Generates.envfrom 1Password (vaultdg) using@dg/cli env generate**migrate**- Runs database migrations via@dg/db(Sequelize)**dev**- Starts the persistent Next.js dev server, watching all app and package files
The dev and prod apps use separate Neon databases, so local testing won't affect production data.
- Homebrew
- 1Password account with vault
dg(one item per env var; value stored in thevaluefield)
Bootstrap uses brew bundle to install dependencies from the Brewfile (nodenv, node-build, 1password-cli, neonctl, cloudflared) and installs turbo globally via npm.
scripts/bootstrap(installs deps, authenticates 1Password + Turbo, generates.env)turbo devto start developing
If turbo is not found, ensure ~/.nodenv/shims is on PATH, then nodenv rehash.
.env is generated from 1Password (vault dg) using [config/env.secrets.keys](config/env.secrets.keys). Each key must be an item with the same name and the value stored in the value field. To regenerate after vault changes: turbo clean && turbo dev or delete .env and re-run turbo dev.
Feature branches squash onto main, and Linear is used for ticket tracking.
- PR creation triggers CI checks and auto-bumps the version (see Versioning below).
- Use the Vercel deploy preview to verify functionality.
- Merges automatically create a GitHub release with the "What changed?" content as notes π
Required GitHub Permissions
The release workflow requires:
- Actions permissions β Workflow permissions β Read and write permissions
- Actions permissions β Allow GitHub Actions to create and approve pull requests (checked)
- Unit tests live in
__tests__folders next to source files and match basenames (Homepage.tsxβ__tests__/Homepage.test.tsx). - Run
turbo test --filter=@dg/webfor web unit tests. - Jest setup is intentionally minimal (
apps/web/jest.setup.tsonly imports@testing-library/jest-domand no-op popover methods for jsdom). - Use
@testing-library/user-eventfor interactions (avoidfireEvent). - CI runs
turbo testvia theπ§ͺ Testjob.
Pretty standard Next.js App Router app. /public has static files, /src/app has pages and API
routes and components, /src/types has global types, /src/hooks has shared hooks, and
/src/services has server-side data fetching.
- Next.js App Router provides routing, Server Components, and API routes. Client calls to
/api/Xhitapp/api/X/route.tson the server. - Vercel hosts + builds the site. Every
maincommit deploys π Env vars mirror.envgenerated from 1Password. - Cloudflare manages DNS/security. Cloudflare's MX records redirect email to Gmail.
- Contentful supplies most content via GraphQL. New content triggers builds via webhook.
- Next.js App Router caching with Cache Components (
use cache) and tag-based revalidation keeps data fresh. - Pigeon Maps powers the homepage map with Stamen Watercolor tiles and terrain labels (via Stadia Maps).
- MUI for UI and styling (
sx, viaemotion). - Contentful schema lives in
packages/content-models/contentful/schema.graphqland is refreshed viaturbo graphql:schemafor editor tooling. Responses are validated with Valibot. - Neon is the Postgres DB for auth tokens + more.
- Sequelize runs migrations + shapes the DB.
- Server Components fetch data directly using server-only functions with Cache Components (
use cache). - Data fetching uses tag-based revalidation for webhook-triggered updates.
- Client Components receive data as props from Server Components.
Spotify + Strava use OAuth, so I use Neon + Sequelize to store and refresh tokens.
There are two tables:
- Token: I grab the latest token, see if it's expired, and if so, fetch new data via Spotify/Strava + the saved refresh token. Once it's persisted, I can call the APIs with the auth tokens. Nice defaults built in so anything missing gives back the right info as possible.
- StravaActivity: I create a row on new activity webhooks, fetch the full activity from Strava, and re-fetch/update JSON when data changes. I track last update time so multiple updates in a window don't hammer Strava's servers.
- Spotify only exposes the 50 most recent plays. Sync runs every 30 minutes via GitHub Actions calling
/api/spotify/sync. - History sync is intentionally disabled until the GDPR import seeds the DB, to avoid gaps.
- Track metadata uses two separate Spotify API patterns:
- Imports (GDPR data): Uses batch
GET /tracks?ids=endpoint (deprecated by Spotify ~March 2026). - Cron sync: Uses individual
GET /tracks/{id}endpoint for ongoing sync after batch access is removed.
- Imports (GDPR data): Uses batch
- The homepage "latest song" card uses
fetchRecentlyPlayed, which returns now-playing or last-played data for UI.
DO NOT hit Strava directly unless you must. Webhooks persist activity data so we read from DB instead.
Each Strava app supports a single subscription, so I keep two apps: one for local testing, one for prod. /api/webhooks handles webhook events, and /console handles OAuth setup and webhook subscription management (dev/test only).
Each app uses its own Neon database, tokens, and callback URLs. OAUTH_CALLBACK_URL should point at /api/oauth (shared by Strava and Spotify), and STRAVA_WEBHOOK_CALLBACK_URL should point at /api/webhooks.
Cloudflare Tunnels (automatic with turbo dev)
The tunnel starts automatically with turbo dev when CLOUDFLARE_TUNNEL_TOKEN is set.
This routes https://dev.dylangattey.com to your local Next.js server for OAuth and webhook testing.
To set up:
- Get the tunnel token from Cloudflare Zero Trust
- Add it to 1Password as
CLOUDFLARE_TUNNEL_TOKEN - Run
turbo envto update your.env
The tunnel will fail if CLOUDFLARE_TUNNEL_TOKEN isn't configured in your .env.
- Start
turbo dev, then visit/consolein the browser to view webhook status and manage subscriptions. Delete subscriptions when done so Strava doesn't ping a dead endpoint. - Add a
console.loginapps/web/src/app/api/webhooks/route.tsand rename an activity to trigger events. Docs
Versioning uses auto-generated version bumps on PRs with GitHub releases on merge. The version is stored in the root package.json and read directly by the app at build time. Check the appropriate checkbox in the PR template (Major/Minor/Patch) - the workflow will auto-commit the version bump to your branch and create a GitHub release on merge with notes from the "What changed?" section.
If multiple PRs each have their own version bump and one merges, other PRs will have conflicts in package.json. To fix:
- Run
turbo drop-bot-commitsto remove bot-authored version bumps and rebase ontoorigin/main - Force push:
git push --force-with-lease - The workflow will automatically create a fresh version bump commit
The repo automatically captures before/after screenshots on PRs comparing production vs the Vercel preview deployment. Screenshots are captured using @vercel/before-and-after and posted as a PR comment.
Edit .github/screenshot-config.json to customize which pages are captured:
{
"productionUrl": "https://dylangattey.com",
"defaultViewport": "1280x800",
"pages": [
{ "path": "/", "name": "Homepage" },
{ "path": "/music", "name": "Music", "viewport": "1280x800" },
{ "path": "/music", "name": "Music (mobile)", "viewport": "375x812" }
]
}Each page can optionally specify:
viewport- Override the default viewport (e.g.,375x812for mobile)selector- CSS selector to capture a specific element instead of full page
If preview deployments have password protection enabled (Vercel Deployment Protection), the workflow will attempt to use a bypass secret. To configure:
- In Vercel: Project Settings > Deployment Protection > Protection Bypass for Automation > Generate secret
- In GitHub: Settings > Secrets and variables > Actions > Add
VERCEL_AUTOMATION_BYPASS_SECRETwith the same value
The workflow will automatically append the bypass parameter to preview URLs. If no secret is configured or bypass fails, screenshots will be skipped with a note on the PR.
Known limitation: When Vercel Deployment Protection is enabled, images in preview screenshots may appear as empty placeholders. This is because the bypass token only authenticates the initial page request, while subsequent image requests through Next.js Image optimization (/_next/image/*) don't include the token. Production screenshots will show all images correctly. To get full image loading in previews, disable Vercel Deployment Protection for preview deployments.