Develop, debug and publish Google Apps Script webapps as a regular website without any frontend restrictions or load delays.
- Provides two robust methods to load your GAS pages depending on your needs.
- Sample, live Apps Script pages illustrate various interaction patterns.
- Meant for GAS webapps that run as the developer (not as the user.)
agents.mdfiles to facilitate your use of AI coding agents.
This monorepo contains two projects, one for the website and another for the GAS. Both projects work together and implement the functionalities for methods #1 and #2. Additionally, the website contains an optional sub-project for a Firebase backend.
- Documentation map
- Method #1: Use a regular frontend (prefered)
- Method #2: Load GAS HTMLService as an iframe
- Additional functionality for both methods #1 and #2
- Firebase Auth
- Demos
- Production Website using this framework
- Directory Structure
- Setup & Configuration common to both projects
- Website project
website/ - Google Apps Script project
google-apps-script/ - Customization
- Messaging Protocol
- License
AGENTS.md– Repository-wide contribution guide. When you edit any directory, read theAGENTS.mdclosest to that file for coding standards.website/AGENTS.md– Build/deploy instructions plus architectural notes for the Vite website (including the optionalfunctions/folder).google-apps-script/AGENTS.md– Bundling, clasp, and iframe/bridge coordination details for the Apps Script project.util-org-sig/readme.md– Companion doc describing how to generate signing keys for the optional multi-org feature.
The coolest one. Completely liberates you from all GAS webapp limitations and load delays but it does not support GAS HTML Templates.
GAS webapps make it hard to call the backend from outside the HTMLService. There is a an execution API but it can only be used if all your GAS backend runs under the user's credentials. You cant use it when running under the developer credentials.
Also, if you try to use doGet returning contentService to publish as an "API", it has two inconveniencea: it will cause a redirect and another fetch from the frontend, to usercontent.google.com, making it slower from the frontend, and requires to return a "JSONP", which needs to be injected as a <script> per call in the frontend, which is slower, more brittle and gives you less control.
This method will insert an iframe only once, and then it handles API calls like a regular frontend call, without redirects or contentService.
- Runs the frontend in the top window, outside of the GAS webapp iframe, loading instantly and without limitations.
- Provides a mirror
google.scriptAPI as a transparent bridge for invoking.gsserver-side functions and loads on-demand or asynchronously at page load time. - Develop, debug an publish the frontend using any tooling, frameworks, libraries or languages (React, Vue, Typescript, Vite, live reloading, etc.)
- Use all the browser functionalities without restrictions imposed by the GAS iframe like localStorage, notifications, credential APIs, serviceWorker, direct URL access, etc.
- Build as SPA, MPA or PWA.
- Reduce GAS load by moving all the frontend serving outside of GAS.
Run an MPA, each page using a different GAS frontend HTMLService. Runs the GAS webapps inside an iframe. Can behave more like a regular frontend by using special helpers to avoid HTMLService limitations. This was the original functionality of the framework and can still be useful if you rely heavily on GAS HTML Templates, otherwise its best to migrate your HTML template to regular HTML and handle the templating from the frontend js, using method #1 to embed.
- Custom Domain Serving: Serves apps under a custom domain, providing more control than Google Sites.
- Analytics Integration: Manages Google Analytics through GTM, receiving events from embedded Apps Scripts.
- Smooth Transitions: Avoids flashing screens by smoothly transitioning page loads on a MPA webapp.
- Responsive Design: Ensures compatibility on both mobile and desktop devices.
- Change the browser header colorscheme to match the script webapp.
- Fullscreen support
- Multi-account Compatibility: Ensures functionality even when users are signed into multiple Google accounts (a long standing issue with GAS HTMLService.)
- Google Workspace Compatibility: Handles redirects typically problematic when users are under a Google Workspace account profile (another long standing issue with GAS HTMLService.)
- Dynamic Multiple Script version Loading: Securely loads different script versions (could be on the same or a different Google Workspace or Google Account) under the same website routes by passing parameters for different "organizations" you can create with the "org/sig" feature.
- Secure domain serving: Only allows your specific domain to embed the apps script iframes.
- Firebase auth including the new Google Sign-in by fedCM with automatic popup and redirect mode fallback without reloading the current page.
- Promise-based calls to google.script. with automatic retries for common communication errors to the server.
- Bundling system for apps script:
- organize files as html, js, css, gs.
- use
.env/.env.localvariables shared in.jsand.gsfiles to better organize and share variables without hardcoded values. - For method #2: bundle into inlined html files to improve performance and comply with the apps script format.
- Website only bundles what you use.
- Logs to GCP Logging: Sends logging events to the parent website, which sends the frontend logs to GCP (same place where the .gs logs go.)
- Easy installation and customization: just
npm installin both the website and apps script directories, customize.env.localfiles andnpm deploy. The website uses vite and the apps script uses a custom npm bundling script andclaspto deploy from the command-line. - Optional helpers to support multiple languages in your website (both in
websiteandHTMLService) usingi18ntags.
Can be used with both hosting methods. I implemented this because I couldnt find a good firebase UI that could be:
- bundable to generate much smaller code.
- handled all possible cases in the auth flow automatically, including redirection when both fedCM and popups are blocked or on older browsers.
It can be used independent of Apps Script. Its a lit component with:
- English & Spanish translations.
- Bundling support (the official FirebaseUI can´t bundle).
- "Google" signin and "Email & Password" signin, including the "verify email flow".
- Extends Firebase Auth with Google FedCM Signing, the newest method with the least user friction.
- Handles failures by automatically retrying with three different methods for Google Signin:
- FedCM: Works even with 3rd party cookies disabled, but needs a newer browser.
- Popup method using the same domain: Works with most browsers, but the user might have manually disabled the popup.
- Redirect method: Works with all browsers. does it without leaving the apps script page.
On the redirect method: If the first two methods fail (browser is too old and popups were blocked) it automatically opens a new login page which handles the login.
It uses the new Firebase sync mechanism indexedDBLocalPersistence and the BroadcastChannel API to communicate with the original "opener" (where the user was trying to log-in) and thus finishes the original login flow. All this is done without refreshing the Apps Script webapp.
On the Apps Script:
- Adds the missing Crypto support in
.gs, to securely validate a firebase idToken. - It can define a page as requiring authentication before loading, or can login on-demand after load.
- It has new messages to request the user to log-in, or get an idToken.
Shows a simple website with three pages.
- Page 1: uses method #2, follows the simplest flow, where the page loading animation stops as soon as the script page loads.
- Page 2: uses method #2 with a more complex flow where the page partially loads while the loading animation (from the parent website) continues. It then loads external libraries and the rest of the page, then stops the parent loading animation.
- Page 3: showcases method #1. Loads the GAS bridge asynchronously, with a button that calls a GAS backend API.
NOTE: The demo websites do not have a public auth (login) API key configured so the demos only show the login UI. You can try the full login features on the production website.
- Demo Website: fir-apps-script.firebaseapp.com
- Using the "org/sig" with sample URL parameters: fir-apps-script.firebaseapp.com?org=xxx&sig=yyy
- Visit Tutor For Me
- Optional login: Do a demo lesson from the homepage. You can view the page without login, then use the login-on-demand feature at the time you save the page.
- Forced login: Tutor For Me | My lessons
- The website only uses embedding method #1 for most pages.
Each folder listed below also ships an AGENTS.md or readme with implementation notes—read them before editing so the bridge/iframe contract stays aligned.
website/: Parent website project managing the bridge, Apps Script embedding, communication, analytics and login.website/functions/: Optional Firebase Cloud Functions (Node.js 22) project.api/logs.jsproxies frontend logs into Google Cloud Logging and can be extended with more endpoints.google-apps-script/: Google Apps Script project (compiles separate from the parent website project).util-org-sig/: Crypto utility functions for the "org/sig" feature. Seeutil-org-sig/readme.mdfor key generation and signing instructions.
clone, then inside website/src and google-apps-script/src, create your .env.local files for each src directory.
npm installatwebsite/and atgoogle-apps-script/. If you change the Firebase function proxy, also runnpm installinsidewebsite/functions/(Cloud Functions require Node.js 22, while Vite/dev builds work with Node.js ≥18).- To use cloud logging for the frontend, use the firebase function in
website/functions/api/logs.js. - Keep shared environment variables (
URL_WEBSITE, Firebase project IDs, and theorg/sigpublic keys) aligned between both.envtrees so the iframe/bridge validation logic matches on each side. - For improved security set the
ALLOWED_HOSTandURL_WEBSITEenvironment variable to the same domain in both.envfiles and setALLOW_ANY_EMBEDDING=falseingoogle-apps-script/.
- Bridge: Implements the mirror
google.scriptAPI. The bridge loads a minimal GAS hidden iframe that handles the API calls (for method #1). - Embedding: Embeds Apps Script web apps using visible iframes that show the GAS webapp frontend (for method #2).
- Custom Domain: Uses Firebase Hosting for domain management and authentication.
- Firebase Auth (UI, login with Google, login with email, script helpers to validate auth id tokens)
- Dynamic Loading: Load scripts dynamically using the
orgURL parameter. - Security: Validates scripts via URL
organdsigparameters using public key signature verification. Seeutil-org-sig/readme.mdfor instructions to create your own public/private key pairs. - Centralized logging: Sends logs from the frontend an backend to the same GCP logging.
- Parent-Iframe Communication for method #2:
- login tokens (idToken)
- URL parameter changes
- Analytics event tracking
- Page title updates
- Load state notifications
- Analytics: Integrated Google Tag Manager (GTM).
- Centralized Logging: Logs iframe error events via Firebase Cloud Functions to Google Cloud Logging.
- Backend: Uses Firebase cloud functions under
functions, it implements theapi/logs/putLogsendpoint to send frontend logs to GCP logging. It can be easily extended to add more API endpoints.
- Do the common setup in the section above.
- install
claspingoogle-apps-script/:npm install @google/clasp -g - create or clone a GAS with
clasp. npm installatgoogle-apps-script/npm run login: authorizesclaspnpm run build: builds and uploads to apps script "dev" environment.- add your apps script production id to "pub-prod" in
package.jsonin thegoogle-apps-script/project npm run deploy: deploys the script to production.
common.js: Core logicindex.html: Landing pagepage1.html,page2.html,page3.html: Iframe hostslogs.js: Server-side loggingfirebase.json: Hosting config
- Bridge to execute
.gsserver function (for method #1) - Enhanced Logging: Captures GAS frontend an backend logs and sends to parent, to the same GCP backend projectl logs.
- Iframe Communication: Manages message passing for method #2 (analytics, login, events, load states).
- Crypto support to securely validate idToken signatures and expiration from Firebase Auth (for method #2)
- Promise-based calls to google.script with automatic retries:
await server.run('myServerFn', arg1, arg2);const loc = await server.getLocation(); - separate html/js/css/gs. Contains npm scripts to bundle, push, use
.env/.env.localand publish withclasp.
- Do the common setup in the section above.
npm installatwebsite/npm run login: authorizes Firebasenpm run dev: live preview on localhost. SeeALLOW_ANY_EMBEDDINGingoogle-apps-script/so GAS can load in localhost.npm run deploy: deploys to Firebase
- Run the website from localhost. This requires
ALLOW_ANY_EMBEDDING=true. The easiest way is to publish to production allowing any embedding. To make it more robust only allow any embedding from the dev build by settingALLOW_ANY_EMBEDDING=trueand doing a build without publishing, thus the dev deploy in GAS will allow embedding while the production deploy will not. You can then set the script id to be the GAS development ID instead of the production ID in.env.local, or by generating a new org/sig pair with the utility inutil-org-sig. This last method allows you to generate a URL so the website loads the dev GAS instead of the default production one, giving you an easy way to run production and dev without having to constantly change and deploy the GAS or website.
For Firebase auth, the script´s crypto implementation automatically downloads and updates the Firebase Certificates needed to verify signatures, and stores it in Script Properties.
- FIREBASE_CERTS_JSON: holds the cert.
- FIREBASE_CERTS_UNTIL: holds its expiration.
-
Page 1 (method #2):
- Initialization notification
- Custom analytics events
- URL parameter changes without refresh
- Login / Logout
- GAS HTMLService is used for the UI.
-
Page 2 (method #2):
- Progressive load notifications
- Title updates
- Custom analytics events
- GAS HTMLService is used for the UI.
-
Page 3 (method #1):
- Frontend lives in the parent as a regular frontend, which calls a
.gsserver function. - GAS HTMLService is not used for the UI.
- Frontend lives in the parent as a regular frontend, which calls a
util.js: Client-side utilitiesutil.gs: Server-side utilities, including the list of your public.gsfunction for method #1, inPUBLIC_FUNCTIONS.page1.html,page1.js: Sample Page 1 (method #2)page2.html,page2.js: Sample Page 2 (method #2)bridge.html,bridge.js: Sample Page 3 (method #1)
- Deploy as Web App (Execute: Me, Access: Anyone)
- Use standard GCP project for centralized logs (instructions) using the same gcp project for this and the firebase project.
- create and configure
.env.localfiles both in website/src and google-apps-script/src base on their respective.envfiles. - Search for
CUSTOMIZE:comments in the repo for key spots to extend to your needs.
You only need to know this protocol if you plan to modify it. The iframe and parent page communicate via postMessage events. The following messages are emitted by the Google Apps Script frontend and processed by website/src/js/common.js, letting you provide further processing if needed. When you add new events, update both website/src/js/common.js and google-apps-script/src/js/util.js/bridge.js so the contract stays in sync.
| Action | From → To | Description | Sample data Payload |
|---|---|---|---|
serverRequest / serverResponse |
parent → iframe → parent | Send and respond to a server request from the frontend (method #1 bridge) | { "type": "FROM_PARENT", "action": "serverRequest", "data": { "functionName": "...", "arguments": [...] }, "idRequest": "..." } and { "type": "FROM_IFRAME", "action": "serverResponse", "data": { "result": ... }, "idRequest": "..." } |
siteInited |
iframe → parent | Tells the parent that the iframe can be displayed, with or without stopping the progress animation | { "data": { "dontStopProgress": false } } |
siteFullyLoaded |
iframe → parent | Used only when siteInited set dontStopProgress: true; tells the parent when to stop the progress animation |
|
titleChange |
iframe → parent | Change the title of the website | { "data": { "title": "new title" } } |
logs |
iframe → parent | Send a logs batch to the parent (which then sends it to GCP logging) | { "data": { "logs": [ { "message": "..." } ] } } |
analyticsEvent |
iframe → parent | Send an analytics event | { "data": { "name": "customEvent" } } |
urlParamChange |
iframe → parent | Change a URL param of the main website | { "data": { "refresh": false, "urlParams": { "lang": "en" } } } |
openUrlWithProps |
iframe → parent | Open or replace a route with specific query/path props (method #2 navigation helper) | { "data": { "pathname": "/lesson", "props": { "step": 2 } } } |
toggleFullscreen |
iframe → parent | Ask the parent to toggle fullscreen mode for smoother demos | |
getUser / logoutUser |
iframe → parent (reply sent via the same event name) | Coordinate Firebase login prompts and user info exchange | { "data": { "force": true, "addIdToken": true } } |
validateDomain |
parent → iframe → parent | Received by the iframe. If the domain is correct, it enables the iframe; otherwise it remains hidden to prevent clickjacking |
validateDomain: The parent page validates the domain after receivingsiteInited, responding withvalidateDomain, then enabling the GAS (and keeping unknown origins hidden to prevent clickjacking).serverRequest/serverResponse: Wire up the method #1 bridge—exposed via the mirroredgoogle.scriptproxy—so.gsfunctions can be invoked from the top-level website.getUser/logoutUser: Allow method #2 pages to show login prompts or sign out from the iframe while reusing the website’s Firebase session state.- The rest are for controlling the iframe load lifecycle and implementing UX features for the GAS frontend in method #2.
This project is released under the MIT License.