How to — deploy
Three independent deploy targets — each repo deploys on its own cadence. There is no single command that deploys all three at once. Coordinate the order based on what changed.
Deploy order for coordinated changes
When a change touches more than one repo, always deploy in this order:
- Backend — additive migrations first; safe to go before anything else.
- Widget — deploys new payload-shape consumers before the dashboard starts writing the new shape.
- Next.js dashboard — last, because it produces the payload fields consumers must already understand.
The cardinal rule: consumer deploys before producer when adding a field, consumer deploys after producer when removing one. This preserves backwards compatibility across the deploy gap.
⚠️ Warning: Never merge the dashboard PR before the backend deploy lands. Calling a 404 endpoint from production is worse than a short feature delay.
1. Deploy the backend (Cloudways via SSH + Makefile)
The backend (dropafinder-app-backend) deploys to Cloudways over SSH.
Run the deploy
cd "/Users/codydavis/Local Sites/dropafinder-app-backend"
make deploymake deploy is defined in Makefile as:
SSH_HOST = cloudways-dropafinder
APP_PATH = /home/locationfinders-api/public_html
deploy:
ssh $(SSH_HOST) "cd $(APP_PATH) && \
php artisan migrate --force && \
php artisan config:cache && \
php artisan route:cache && \
php artisan view:cache"It SSHs to cloudways-dropafinder (an alias in ~/.ssh/config) and runs four commands in sequence:
migrate --force— runs any pending migrations.config:cache— bakes environment variables into the config cache.route:cache— compiles all routes to a single file.view:cache— pre-compiles Blade views.
Verify
curl -H "Authorization: Bearer $TOKEN" https://api.locationfinders.com/api/meExpected: a 200 response with user data (not a 500 or routing error).
Cache clear (if needed)
If a deploy produces unexpected behavior, clear caches without re-running migrations:
make cache-clearThis runs config:clear, cache:clear, route:clear, and view:clear.
SSH direct access
make sshDrops you into an interactive SSH session on the Cloudways server for debugging.
2. Deploy the Next.js dashboard (Vercel via git push)
The dashboard (dropafinder-app-nextjs) deploys automatically on every push to main. Vercel watches the repository and triggers a build + deploy on each push.
Run the deploy
cd "/Users/codydavis/Local Sites/dropafinder-app-nextjs"
git push origin mainNo manual deploy command is required.
Monitor
Open the Vercel dashboard for the project to watch the build log in real time. The build typically completes in 1–3 minutes. A failed build does not affect production — Vercel keeps the last successful deployment live.
Verify
- Open the production URL once the Vercel build status shows “Ready”.
- Log in and navigate to a Finder. Open the Finder Builder.
- Confirm the changed feature behaves as expected in production.
💡 Tip: For changes that affect the Live Preview iframe, verify there specifically — it runs the widget inside the iframe and has a separate initialization path.
3. Deploy the widget (R2 via deploy-r2.mjs)
The widget (dropafinder-app-external/ext/2/source/) is a static JS bundle uploaded directly to Cloudflare R2. The CDN serves snippet.js from https://cdn.locationfinders.com/.
Run the deploy
cd "/Users/codydavis/Local Sites/dropafinder-app-external/ext/2/source"
npm run deploy:02This is defined in package.json as:
"deploy:02": "npm run build && node scripts/deploy-r2.mjs"Steps it performs:
npm run build— Vite production build, outputsext/2/snippet.jsand the root-levelsnippet.js.deploy-r2.mjs— reads R2 credentials from.env.development, then uploads two objects to R2:snippet.js— root-level alias (https://cdn.locationfinders.com/snippet.js),max-age=300, must-revalidateext/2/snippet.js— versioned path, same TTL
Required env vars
Set these in .env.development (or as shell env vars) before deploying:
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_ENDPOINT_URL_S3= # Cloudflare R2 endpoint
AWS_REGION=auto
CDN_BASE_URL=https://cdn.locationfinders.comCache propagation
The CDN TTL for snippet.js is 300 seconds (5 minutes). Visitors loading the widget within that window may still get the previous bundle. This is by design — the short TTL ensures rapid propagation without requiring a manual cache purge for routine deploys.
Verify
After 5 minutes, open a page embedding the widget in a private/incognito browser window and confirm the updated behavior is live.
🔴 [NEEDS CLARIFICATION: Is there a manual Cloudflare cache purge step for the widget deploy, or does the 5-minute TTL make it unnecessary in all cases?]
Rollback
| Service | Rollback method |
|---|---|
| Backend | make ssh → git log → git checkout <previous-commit> + make deploy (migrations are forward-only — coordinate if the change involved a destructive migration) |
| Dashboard | Vercel dashboard → Deployments → select a previous deployment → “Promote to Production” |
| Widget | Re-run npm run deploy:02 from the previous git commit |
Where to next
- Run locally before deploying: run-locally
- Full end-to-end deploy flow: add-api-endpoint-end-to-end
- Write tests before shipping: test