These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

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:

  1. Backend (dropafinder-app-backend → Cloudways) — API changes land first.
  2. Widget (dropafinder-app-external → Cloudflare R2) — Widget picks up new API contracts.
  3. 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

ServiceDomainHost
Frontend app🔴 [NEEDS CLARIFICATION: production domain — app.dropafinder.com is unconfirmed; the brand split between DropAFinder and LocationFinders is unclear]Vercel
Backend APIapi.locationfinders.comCloudways
Widget CDNcdn.locationfinders.comCloudflare 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:

FieldValue
projectIdprj_hQGyTOQDvUxvlzW08y8RbzziLRrl
orgIdteam_RdvnJIkSsk7w7lTdgoyW2NAG
projectNameproject-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.

VariableEnvironmentDescription
NEXT_PUBLIC_API_URLProductionBackend API base URL. Production value: https://api.locationfinders.com/api/
NEXT_PUBLIC_GOOGLE_KEYAllGoogle Maps JavaScript API key
NEXT_PUBLIC_TOMTOM_KEYAllTomTom Maps SDK key
NEXT_PUBLIC_MAPBOX_KEYAllMapbox GL JS token
NEXT_PUBLIC_HERE_KEYAllHERE Maps API key
NEXT_PUBLIC_PADDLE_ENVIRONMENTProductionproduction (set to sandbox in preview/dev)
NEXT_PUBLIC_PADDLE_CLIENT_TOKENProduction🔴 [NEEDS CLARIFICATION: production Paddle client token — .env.local holds the sandbox token]
NEXT_PUBLIC_PADDLE_BRONZE_PRODUCT_IDProduction🔴 [NEEDS CLARIFICATION: production Paddle product ID]
NEXT_PUBLIC_PADDLE_BRONZE_PRICE_IDProduction🔴 [NEEDS CLARIFICATION: production Paddle price ID]
NEXT_PUBLIC_PADDLE_PREMIUM_PRODUCT_IDProduction🔴 [NEEDS CLARIFICATION: production Paddle product ID]
NEXT_PUBLIC_PADDLE_PREMIUM_PRICE_IDProduction🔴 [NEEDS CLARIFICATION: production Paddle price ID]
NEXT_PUBLIC_APP_VERSIONAllInjected 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

PreviewProduction
TriggerPush to any non-main branchPush to main
URLAuto-generated Vercel URLConfigured custom domain
Paddle envShould use sandboxMust use production
NEXT_PUBLIC_API_URLCan point to local/staging APIMust 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-dropafinder in ~/.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_html

Steps

# 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 deploy

The 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-clear

This 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:

  1. git revert or git reset the offending commits in the repo.
  2. Re-run make deploy to push the reverted code.
  3. 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.

VariableDescription
APP_ENVproduction
APP_KEYLaravel application key — generate with php artisan key:generate
APP_URLhttps://api.locationfinders.com
APP_DEBUGfalse in production
DB_CONNECTIONmysql (SQLite default in .env.example is for local dev only)
DB_HOST🔴 [NEEDS CLARIFICATION: Cloudways MySQL host]
DB_PORT3306
DB_DATABASE🔴 [NEEDS CLARIFICATION: production database name]
DB_USERNAME🔴 [NEEDS CLARIFICATION: production DB user]
DB_PASSWORD🔴 [NEEDS CLARIFICATION: production DB password]
QUEUE_CONNECTIONdatabase
CACHE_STOREdatabase
R2_ACCESS_KEY_IDCloudflare R2 API token key ID — used by ExternalController to upload finder payloads
R2_SECRET_ACCESS_KEYCloudflare R2 API token secret
R2_BUCKETR2 bucket name
R2_ENDPOINTR2 endpoint URL (e.g., https://<account-id>.r2.cloudflarestorage.com)
CDN_BASE_URLhttps://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:02

deploy:02 expands to npm run build && node scripts/deploy-r2.mjs.

What gets uploaded

Two files are uploaded on every deploy:

R2 keySource 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:02

The 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

VariableRequiredDescription
R2_ACCOUNT_IDYesCloudflare account ID
R2_ACCESS_KEY_IDYesR2 API token access key ID
R2_SECRET_ACCESS_KEYYesR2 API token secret
R2_BUCKETYesR2 bucket name
CLOUDFLARE_ZONE_IDNoZone ID for CDN cache purge (also accepts CF_ZONE_ID)
CLOUDFLARE_API_TOKENNoCF API token with cache purge scope (also accepts CF_API_TOKEN)
CDN_BASE_URLNoDefaults to https://cdn.locationfinders.com
R2_REGIONNoDefaults to auto
R2_ENDPOINTNoDefaults to https://<R2_ACCOUNT_ID>.r2.cloudflarestorage.com
DEPLOY_DRY_RUNNoSet 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:

  1. Check out the previous widget commit.
  2. Re-run npm run deploy:02 to 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 domainDevProduction
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/apihttps://api.locationfinders.com/api (no trailing slash — this is intentional per each env file)
CDN base URLN/A (widget built for dev points at local API)https://cdn.locationfinders.com
Paddle environmentsandboxproduction
APP_ENV (Laravel)localproduction
APP_DEBUG (Laravel)truefalse

🔴 [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.]