Deployments
Three repos, three hosting targets. This page is the operational reference for deploying each one. Read it before touching production.
Related reading: Architecture Overview · Storage and CDN · Run Locally
Deployment order
When releasing changes that span multiple repos, always deploy in this order:
- Backend (
dropafinder-app-backend→ Cloudways) — API changes land first. - Widget (
dropafinder-app-external→ Cloudflare R2) — Widget picks up new API contracts. - Next.js (
dropafinder-app-nextjs→ Vercel) — App points at the updated API and widget.
Reversing this order can cause the live app to call API endpoints that do not yet exist, or serve a widget that cannot talk to the new backend.
Domain map
| Service | Domain | Host |
|---|---|---|
| Frontend app | 🔴 [NEEDS CLARIFICATION: production domain — app.dropafinder.com is unconfirmed; the brand split between DropAFinder and LocationFinders is unclear] | Vercel |
| Backend API | api.locationfinders.com | Cloudways |
| Widget CDN | cdn.locationfinders.com | Cloudflare R2 |
1. Next.js → Vercel
How it deploys
Vercel is connected to the dropafinder-app-nextjs git repository. Every push to main triggers an automatic production deployment. Every push to a non-main branch generates a preview deployment at a unique URL.
There is no vercel deploy command needed in normal workflow — the git push is the deploy action.
Vercel project identifiers
From .vercel/project.json:
| Field | Value |
|---|---|
projectId | prj_hQGyTOQDvUxvlzW08y8RbzziLRrl |
orgId | team_RdvnJIkSsk7w7lTdgoyW2NAG |
projectName | project-u64n2 |
Steps
# 1. Commit and push your changes to main
git push origin main
# 2. Monitor the deployment in the Vercel dashboard or via CLI
vercel ls # list recent deployments (optional, requires vercel CLI)Preview deployments are created automatically for all other branches. Share the preview URL with reviewers before merging.
Rollback
In the Vercel dashboard: Deployments → select an older deployment → Promote to Production. No CLI command needed.
⚠️ Warning: Vercel rollback does not roll back environment variable changes. If you updated env vars in the dashboard, restore them manually before promoting the old build.
Required environment variables
Set all of these in the Vercel dashboard under Project → Settings → Environment Variables. Variables prefixed NEXT_PUBLIC_ are embedded in the client bundle at build time.
| Variable | Environment | Description |
|---|---|---|
NEXT_PUBLIC_API_URL | Production | Backend API base URL. Production value: https://api.locationfinders.com/api/ |
NEXT_PUBLIC_GOOGLE_KEY | All | Google Maps JavaScript API key |
NEXT_PUBLIC_TOMTOM_KEY | All | TomTom Maps SDK key |
NEXT_PUBLIC_MAPBOX_KEY | All | Mapbox GL JS token |
NEXT_PUBLIC_HERE_KEY | All | HERE Maps API key |
NEXT_PUBLIC_PADDLE_ENVIRONMENT | Production | production (set to sandbox in preview/dev) |
NEXT_PUBLIC_PADDLE_CLIENT_TOKEN | Production | 🔴 [NEEDS CLARIFICATION: production Paddle client token — .env.local holds the sandbox token] |
NEXT_PUBLIC_PADDLE_BRONZE_PRODUCT_ID | Production | 🔴 [NEEDS CLARIFICATION: production Paddle product ID] |
NEXT_PUBLIC_PADDLE_BRONZE_PRICE_ID | Production | 🔴 [NEEDS CLARIFICATION: production Paddle price ID] |
NEXT_PUBLIC_PADDLE_PREMIUM_PRODUCT_ID | Production | 🔴 [NEEDS CLARIFICATION: production Paddle product ID] |
NEXT_PUBLIC_PADDLE_PREMIUM_PRICE_ID | Production | 🔴 [NEEDS CLARIFICATION: production Paddle price ID] |
NEXT_PUBLIC_APP_VERSION | All | Injected automatically from package.json via next.config.ts — do not set manually |
💡 Tip: The local .env.local uses sandbox Paddle credentials. Make sure the Vercel production environment uses NEXT_PUBLIC_PADDLE_ENVIRONMENT=production and real product/price IDs, not the sandbox values from .env.local.
Preview vs. production
| Preview | Production | |
|---|---|---|
| Trigger | Push to any non-main branch | Push to main |
| URL | Auto-generated Vercel URL | Configured custom domain |
| Paddle env | Should use sandbox | Must use production |
NEXT_PUBLIC_API_URL | Can point to local/staging API | Must point to https://api.locationfinders.com/api/ |
🔴 [NEEDS CLARIFICATION: Is there a staging environment for the backend? No staging deploy script or env file is visible in any repo. Preview deployments currently appear to run against the same production API.]
2. Backend → Cloudways
How it deploys
The backend is a Laravel application hosted on Cloudways managed PHP hosting. Deployment is SSH-driven via the Makefile at the repo root. There is no CI pipeline — you run make deploy from your local machine.
Prerequisites:
- SSH access to the Cloudways host configured under the alias
cloudways-dropafinderin~/.ssh/config. - Your SSH public key added to the Cloudways server.
💡 Tip: To set up the SSH alias, add a block like this to ~/.ssh/config:
Host cloudways-dropafinder
HostName <cloudways-server-ip>
User <cloudways-ssh-user>
IdentityFile ~/.ssh/your_key🔴 [NEEDS CLARIFICATION: Exact Cloudways server IP, SSH username, and which key to use. Retrieve from the Cloudways dashboard under Server → SSH/SFTP.]
App path
/home/locationfinders-api/public_htmlSteps
# From your local machine, in the backend repo root.
# Optionally verify SSH connectivity first:
make ssh # opens an interactive SSH session — exit with Ctrl-D
# Run the deploy:
make deployThe make deploy target runs the following commands on the server in a single SSH call:
cd /home/locationfinders-api/public_html \
&& php artisan migrate --force \
&& php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache⚠️ Warning: php artisan migrate --force runs database migrations immediately without prompting. Ensure your migration is backward-compatible before deploying — the app will serve live traffic during the migration.
After deploying code changes
If you changed queue-handled jobs or workers, restart the queue after deploying:
ssh cloudways-dropafinder \
"cd /home/locationfinders-api/public_html && php artisan queue:restart"Queue workers will finish their current job and then restart cleanly, picking up the new code.
Clearing cache without redeploying
If you need to clear cached config/routes/views without running a full deploy (e.g., after a .env change on the server):
make cache-clearThis runs config:clear, cache:clear, route:clear, and view:clear in sequence.
⚠️ Warning: make cache-clear leaves the app uncached until the next request regenerates the cache. On high-traffic deployments, follow immediately with make deploy (which re-caches everything) or run the cache commands manually.
Rollback
There is no automated rollback. To roll back:
git revertorgit resetthe offending commits in the repo.- Re-run
make deployto push the reverted code. - If the migration was destructive, restore from the Cloudways database backup taken before the deploy.
💡 Tip: Cloudways takes automatic daily database backups. Before any migration that drops columns or tables, verify that today’s backup has completed in the Cloudways dashboard.
Required environment variables
The backend reads from a .env file at the app root (/home/locationfinders-api/public_html/.env). This file is not in the repo — it must be managed on the server directly (via SSH) or through the Cloudways environment panel.
| Variable | Description |
|---|---|
APP_ENV | production |
APP_KEY | Laravel application key — generate with php artisan key:generate |
APP_URL | https://api.locationfinders.com |
APP_DEBUG | false in production |
DB_CONNECTION | mysql (SQLite default in .env.example is for local dev only) |
DB_HOST | 🔴 [NEEDS CLARIFICATION: Cloudways MySQL host] |
DB_PORT | 3306 |
DB_DATABASE | 🔴 [NEEDS CLARIFICATION: production database name] |
DB_USERNAME | 🔴 [NEEDS CLARIFICATION: production DB user] |
DB_PASSWORD | 🔴 [NEEDS CLARIFICATION: production DB password] |
QUEUE_CONNECTION | database |
CACHE_STORE | database |
R2_ACCESS_KEY_ID | Cloudflare R2 API token key ID — used by ExternalController to upload finder payloads |
R2_SECRET_ACCESS_KEY | Cloudflare R2 API token secret |
R2_BUCKET | R2 bucket name |
R2_ENDPOINT | R2 endpoint URL (e.g., https://<account-id>.r2.cloudflarestorage.com) |
CDN_BASE_URL | https://cdn.locationfinders.com |
JWT_SECRET | 🔴 [NEEDS CLARIFICATION: JWT signing secret — see config/jwt.php] |
MAIL_MAILER | 🔴 [NEEDS CLARIFICATION: production mail driver] |
MAIL_FROM_ADDRESS | 🔴 [NEEDS CLARIFICATION: production from address] |
3. Widget → Cloudflare R2
How it deploys
The widget (dropafinder-app-external, extension version 2) is a Vite-built JavaScript bundle. Deployment uploads the compiled files directly to a Cloudflare R2 bucket and then purges the Cloudflare CDN cache for the affected URLs.
The deploy script is ext/2/source/scripts/deploy-r2.mjs. It is invoked via:
cd ext/2/source
npm run deploy:02deploy:02 expands to npm run build && node scripts/deploy-r2.mjs.
What gets uploaded
Two files are uploaded on every deploy:
| R2 key | Source path (relative to script) | Cache-Control |
|---|---|---|
snippet.js | ../../../../snippet.js (repo root snippet) | public, max-age=300, must-revalidate |
ext/2/snippet.js | ../../snippet.js (ext/2 compiled bundle) | public, max-age=300, must-revalidate |
Both files are served under https://cdn.locationfinders.com/. The 5-minute max-age means stale copies may be served for up to 5 minutes after deploy even after the cache purge (existing user sessions that already cached the file locally).
Steps
# 1. From the ext/2/source directory
cd "/path/to/dropafinder-app-external/ext/2/source"
# 2. (Optional) Dry run — prints what would be uploaded, skips actual upload and purge
DEPLOY_DRY_RUN=1 npm run deploy:02
# 3. Deploy to production
npm run deploy:02The script loads credentials from ext/2/source/.env.deploy.local if that file exists. Any variable already set in the shell environment takes precedence over values in that file.
Setting up credentials
Create ext/2/source/.env.deploy.local (this file is gitignored):
R2_ACCOUNT_ID=<your-cloudflare-account-id>
R2_ACCESS_KEY_ID=<r2-api-token-key-id>
R2_SECRET_ACCESS_KEY=<r2-api-token-secret>
R2_BUCKET=<bucket-name>
# Optional — only needed for cache purge
CLOUDFLARE_ZONE_ID=<zone-id-for-locationfinders.com>
CLOUDFLARE_API_TOKEN=<cf-api-token-with-cache-purge-permission>
# Optional — defaults to https://cdn.locationfinders.com
# CDN_BASE_URL=https://cdn.locationfinders.com🔴 [NEEDS CLARIFICATION: Exact R2 bucket name and Cloudflare account ID. Retrieve from the Cloudflare dashboard.]
💡 Tip: The Cloudflare cache purge step is optional. If CLOUDFLARE_ZONE_ID and CLOUDFLARE_API_TOKEN are absent, the script uploads successfully and logs a warning that the purge was skipped. The CDN will serve stale files until they expire (5-minute TTL).
Required environment variables
| Variable | Required | Description |
|---|---|---|
R2_ACCOUNT_ID | Yes | Cloudflare account ID |
R2_ACCESS_KEY_ID | Yes | R2 API token access key ID |
R2_SECRET_ACCESS_KEY | Yes | R2 API token secret |
R2_BUCKET | Yes | R2 bucket name |
CLOUDFLARE_ZONE_ID | No | Zone ID for CDN cache purge (also accepts CF_ZONE_ID) |
CLOUDFLARE_API_TOKEN | No | CF API token with cache purge scope (also accepts CF_API_TOKEN) |
CDN_BASE_URL | No | Defaults to https://cdn.locationfinders.com |
R2_REGION | No | Defaults to auto |
R2_ENDPOINT | No | Defaults to https://<R2_ACCOUNT_ID>.r2.cloudflarestorage.com |
DEPLOY_DRY_RUN | No | Set to 1 to preview actions without uploading or purging |
Rollback
There is no versioned rollback for the widget. The R2 upload overwrites the existing files in place. To roll back:
- Check out the previous widget commit.
- Re-run
npm run deploy:02to overwrite with the older build.
⚠️ Warning: Because the snippet files are not versioned by filename (they are always snippet.js and ext/2/snippet.js), a bad deploy is live immediately for any page load that misses the local browser cache. Prioritize testing with DEPLOY_DRY_RUN=1 before deploying to production.
💡 Tip: The finder payload files (the per-finder JSON blobs that the widget fetches at runtime) are hash-versioned and immutable. Rolling back the widget does not roll back finder payload content — those are managed independently via the Publish flow in the builder. See Storage and CDN for details.
Environment summary
| Variable domain | Dev | Production |
|---|---|---|
| Backend API URL (Next.js) | http://127.0.0.1:8000/api/ | https://api.locationfinders.com/api/ |
| Backend API URL (Widget) | http://127.0.0.1:8000/api | https://api.locationfinders.com/api (no trailing slash — this is intentional per each env file) |
| CDN base URL | N/A (widget built for dev points at local API) | https://cdn.locationfinders.com |
| Paddle environment | sandbox | production |
APP_ENV (Laravel) | local | production |
APP_DEBUG (Laravel) | true | false |
🔴 [NEEDS CLARIFICATION: No staging environment is visible in any repo. If one exists, its deploy scripts, env vars, and target hosts are not documented here.]