The Problem
Pharmacy Decoder is a single-file web app. It serves 340 UMPJE practice questions to pharmacy students. Schools get access codes that unlock 14 days of free use. Students verify their school email via a one-time password sent to their inbox.
That OTP flow needs server-side logic. You cannot send emails from the browser. You cannot verify codes securely in client-side JavaScript. The question was: what is the simplest possible backend that handles this without introducing a build system, a deployment pipeline, or a monthly hosting bill?
Why Google Apps Script
Google Apps Script is JavaScript that runs on Google's infrastructure. It can send emails via Gmail, read and write to Google Sheets, and expose web endpoints via the doGet and doPost functions.
One deployment setting deserves its own warning. For a browser to call the endpoint without a Google login, you have to deploy with Execute as set to Me and Who has access set to Anyone. Google's documentation is blunt about what that means: the script always runs with the owner's permissions regardless of who calls it. The endpoint URL sits in your client side JavaScript, so treat it as public. Anonymous strangers are running code with your Gmail send authority and your access to every Sheet the script is authorised for. Scope the script to the one Sheet it needs, never return an OAuth token or any account data to the client, validate every input as hostile, and keep the code path reachable by anonymous callers as small as you can make it. The Apps Script runtime itself carries no separate charge. Be clear about which tier you are on, though, because the higher email quota comes with a paid Google Workspace subscription and the free consumer tier is capped far lower.
For Pharmacy Decoder, Apps Script handles three things:
First, OTP generation and delivery. When a student enters their school email and access code, the app calls Apps Script. Keep every secret on the server side of that call. Valid access codes and any shared key belong in Script Properties or in the Sheet, never in the client bundle, because a single file web app ships its entire source to the browser. Script Properties are not a vault either. They are shared by all users of the script and readable by anyone with edit access to the project. The script generates a 6-digit code, stores it with an expiration timestamp, and sends it to the student's inbox via Gmail. Six digits is only a million values, so an open endpoint needs the rest of the design around it: cap verification attempts per address and lock the address out after a handful of failures, invalidate the code the moment it is used or expires, and throttle how often the same address or source can request a new one. Without those, the endpoint is both brute forceable and a way for a stranger to send mail from your account.
Second, OTP verification. The student enters the code. The app calls Apps Script again. The script checks the code against the stored value and expiration. If valid, it returns success.
Third, activation logging. On successful verification, the script writes a row to a Google Sheets tab: full name, email, access code, graduation year, exam date, follow-up date, activation timestamp. That row is identifiable student data, so it comes with obligations the architecture does not remove. None of it is PHI, so HIPAA is not in play here. FERPA is the one to think about: because schools hand out the access codes, this is a service students use as part of a school activity, and the Department of Education's guidance on online educational services treats that differently from a tool a student signs up for on their own. Get a written agreement with each participating school, tell students at the point of entry what you collect and how long you keep it, get real consent before you use that follow-up date to contact anyone, set a deletion schedule, and check the Sheet's Drive sharing settings, because on a Sheet the sharing dialog is your entire access control.
The JSONP Pattern
Apps Script endpoints do not support CORS in the way a typical API does. The standard fetch API with no-cors mode cannot read the response body. The solution is JSONP: dynamic script tag injection.
The app creates a script element, sets the src to the Apps Script URL with a callback parameter, and appends it to the document. Apps Script wraps the response in a function call that matches the callback name. When the script loads, the browser executes the callback with the response data.
This pattern is old, and it is not free of risk. Google's own Content Service documentation warns that because anyone can embed the script tag in their page, a malicious site can trigger your endpoint and capture what it returns. For an endpoint that sends OTP emails and returns verification results, that means treating the endpoint as fully public and hostile: rate limit per email address and per source, never return anything in the response that is not safe for an attacker to read, and never return the OTP itself. Worth knowing there is a second option: a simple POST with Content-Type set to text/plain is not preflighted, so fetch can read the response body normally without the script tag risk.
What This Costs
Nothing so far. Google Apps Script caps email sending at 100 recipients per day on a consumer account and 1,500 per day on a paid Google Workspace account. Runtime is capped per invocation at 6 minutes, not as a daily pool, and simultaneous executions are capped at 30 per user. That last number is the one to watch, because a web app deployed to run as its owner charges every visitor's request to that one account. Whether you stay under those limits depends entirely on your send pattern. A single cohort of a few hundred students onboarding in the same week will exhaust a 100 per day consumer quota, and every resend counts against it. Check MailApp.getRemainingDailyQuota() before sending, and decide in advance what the app tells a student when the quota is gone.
Compare this to a Vercel serverless function, which would work but introduces a build step, a deployment pipeline, environment variables, and a framework dependency. For what Pharmacy Decoder needs, that is unnecessary complexity.
When Apps Script Is the Wrong Choice
Apps Script is wrong when you need real-time performance, complex routing, or heavy computation. It is wrong when you need WebSocket connections or streaming responses. It is wrong when your app grows to hundreds of concurrent users.
For a pharmacy exam prep app that serves a few hundred students per school cohort with occasional OTP verification requests, it is the laziest correct solution.
The Takeaway
Not every app needs a full backend. Google Apps Script gives you server-side email sending, data storage via Sheets, and web endpoints for free. For small, focused apps where the backend logic is simple, it is a tool worth knowing. The FERPA and quota points above are general information, not legal advice.
