Diagnosing and Staging a Critical 403/404 Outage in Apps Script Deployment: The Sailjada Reserve Widget Incident

Executive Summary

During a routine status check across the Sailjada booking infrastructure, we discovered that the primary Apps Script endpoint powering the "Reserve" widget—deployed across 11 event pages—was returning HTTP 403 (Forbidden) and 404 (Not Found) responses. This represented a total stoppage on inbound booking deposits, silently failing every customer attempting to reserve a charter. This post details the diagnosis, root cause analysis, and the staging of the fix.

What Was Done

We identified a permissions misconfiguration in the Google Apps Script deployment that had caused the public-facing `/exec` endpoint to become inaccessible. The fix required:

  • Live HTTP status verification of both Apps Script endpoints
  • Confirmation that the deployment ID and execution URL were correctly mapped
  • Adjustment of deployment access controls from a restricted state back to public execution
  • Staging the corrected deployment without breaking the existing URL contract across 11 pages

Technical Details: The Deployment Architecture

Apps Script Project Structure

The Sailjada booking system uses a Google Apps Script project (ID: 1dDpSK8JZda7XUpKIGlyyAX19KLL4JqFjYVtpcunB5ZE3-NMX_9v0lQJ5) to handle form submissions and deposit processing. This project has a single published deployment with the public-facing endpoint:

https://script.google.com/macros/s/AKfycbw...44Pme8wCA/exec

The endpoint is called by JavaScript embedded in 11 event pages across two primary domains:

  • sailjada.com (5 charter event pages)
  • queenofsandiego.com (6 charter event pages)

Each page contains a form submission handler that POSTs to this single endpoint with passenger name, email, phone, and deposit amount. The Apps Script backend validates the input, creates a Stripe Payment Intent, and returns a client token for the frontend to complete the charge.

The Root Cause: Deployment Access Control

Google Apps Script deployments have two critical permission layers:

  • Who has access: Controls whether the `/exec` endpoint is public, restricted to a domain, or limited to specific users
  • Execute as: Determines which user's permissions the script runs under (typically "Me" for script owner or "User accessing the app")

The outage occurred because the deployment's "Who has access" setting had been restricted from "Anyone" (public) to a more limited access model, likely during a prior security hardening attempt. This caused the public-facing `/exec` URL to reject all unauthenticated requests with a 403 Forbidden response.

The secondary 404 responses on some pages were caused by stale or malformed endpoint URLs cached in older page versions—a cascading effect of the primary outage.

Diagnosis Process: HTTP Status Verification

We verified the live state of both endpoints using standard HTTP monitoring:

curl -I https://script.google.com/macros/s/AKfycbw...44Pme8wCA/exec

Initial result: HTTP 403 Forbidden

We then cross-checked by examining the Apps Script project's deployment manifest in the Google Cloud Console:

  • Navigated to script.google.com
  • Opened the project: 1dDpSK8JZda7XUpKIGlyyAX19KLL4JqFjYVtpcunB5ZE3-NMX_9v0lQJ5
  • Selected Deploy → Manage deployments
  • Reviewed the active deployment configuration

This confirmed that the deployment's access control was set to a restricted mode rather than public execution.

Key Infrastructure Decisions

Why We Didn't Redeploy to a New Slot

A critical decision point: should we create a new deployment with a different `/exec` URL, or fix the existing one in place?

We chose in-place remediation because:

  • URL stability: Changing the endpoint URL would require modifying JavaScript across 11 pages in two separate repositories. This introduces merge risk and deployment coordination overhead.
  • No code changes required: The bug was purely a configuration issue, not a code defect. Redeploying code unnecessarily would risk introducing new issues.
  • Zero client-side downtime: By keeping the URL constant, cached JavaScript in browsers and CloudFront edge caches remain valid. No cache invalidation required.

The fix involved only adjusting the deployment's access control settings in the Google Cloud Console—no new code deploy, no URL migration.

Execution Context: "Execute as Me"

We also confirmed that the deployment was set to Execute as Me (the script owner), not User accessing the app. This is the correct pattern for this use case because:

  • The Apps Script needs to access the project's Apps Script Properties (environment config like Stripe API keys)
  • The script does not need to read or write to end-user Google Drive or other personal data
  • It simplifies permission debugging—execution failures are due to the script owner's permissions, not the calling user

Staging the Fix

The fix was staged but not yet deployed to production. The steps to remediate are:


1. Navigate to script.google.com
2. Open project ID: 1dDpSK8JZda7XUpKIGlyyAX19KLL4JqFjYVtpcunB5ZE3-NMX_9v0lQJ5
3. Click Deploy → Manage deployments
4. Click the pencil icon on the active deployment
5. Change "Who has access" from restricted setting → "Anyone"
6. Confirm "Execute as" = "Me"
7. Click "Deploy"

After this change, the endpoint will return HTTP 200 for valid POST requests and process deposits normally. No changes to client-side code are required.

Impact Assessment

Revenue impact: Every booking attempt across 11 event pages has been silently failing. We cannot measure exact lost revenue because the funnel is dark (no error logging at the client level for failed form submissions). However, this is a total stoppage—every reserve button click during the outage period resulted in a failed deposit.

Detection latency: The outage was caught during a scheduled infrastructure health check, not by customer reports. This suggests either:

  • Low booking volume during the outage window, or
  • Customers attempted bookings but did not report failures, chalking it up to connectivity issues

This highlights the need for synthetic monitoring