Architecting for Trust: Building a Secure, Credit-Based SaaS
-
Landing Page
The SaaS landing page, designed to highlight features and drive conversions.
-
Authentication & Dashboard
Secure Google OAuth via Supabase and the user's primary dashboard for tracking active credits.
-
Stripe Payments
A complete integration with Stripe, managing one-off credit top-ups and recurring subscriptions.
-
API Key Management
Functional API key generation and CRUD operations. Keys are hashed securely within the system.
Introduction
Building a wrapper around an API is relatively simple. Building a secure, monetized, and rate-limited SaaS application around that API is a different challenge entirely.
My latest project is a Node.js-backed Airtable extension designed to transform database records using AI. While Airtable offers native "AI fields," they operate as a black box. My goal was to build a tool that gives businesses granular control over their token usage, allowing them to optimize costs while processing data via functional, hashed API keys.
Deploying this required integrating Stripe for tiered subscriptions, Supabase for OAuth, and a custom Node.js backend to handle complex financial and state logic. This post explores the architectural decisions, security boundaries, and distributed state challenges I encountered along the way.
The Security Perimeter: Never Trust the Client
From day one, I designed the frontend (hosted on Netlify) as a "dumb" consumer. The UI exists solely for display purposes; it has no authority over the user's state, plan, or credit balance. Every critical action is validated against a session-based source of truth on the backend.
To minimize risk, I deliberately separated identity management from application data. Supabase handles Google OAuth and secure session management, meaning my custom database never touches highly sensitive Personally Identifiable Information (PII) like passwords.
Furthermore, because the Airtable extension relies on API keys, I implemented SHA-256 hashing. Even in the event of a database breach, the keys remain secure because the validation salt is isolated within Railway environment variables.
API Hardening & Cost Protection
When dealing with LLM integrations, an exposed or abused API key isn't just a security risk—it's a financial one. To mitigate this, I implemented strict API Key Scoping.
Beyond just server-side environment variables, I configured the Gemini API provider to reject any requests originating from outside my verified production domains. By restricting the HTTP Referrer and IP origin, I ensured that even if a key were somehow intercepted, it would be functionally useless in an unauthorized environment. This multi-layered defense prevents "API hijacking" and protects the project from unexpected cost spikes.
Financial Engineering & Webhook Resilience
Handling user payments and credits requires strict data integrity. The system utilizes Stripe for processing transactions—such as buying one-off credits or subscribing to monthly top-up plans.
Because webhooks dictate the application's lifecycle (e.g., granting credits or locking
an account upon a failed payment), securing the /webhook endpoint was critical.
I implemented Stripe Signature verification to ensure every incoming request genuinely originated
from Stripe, preventing malicious actors from replaying payloads to grant themselves free credits.
The Credit Reservation Pattern
One of the most interesting challenges was preventing race conditions. What happens if a user fires off dozens of API requests in the same millisecond? Without safeguards, they could exhaust resources and bankrupt the system.
To solve this, I implemented atomic database transactions using a Credit Reservation Pattern:
- When an API request arrives, the backend immediately "locks" or reserves the required credits.
- The heavy processing (data transformation) occurs.
- Depending on the outcome, the system either officially resolves (deducts) the credits or releases (refunds) them back to the user's pool.
This guarantees that a user cannot bypass their strict token limits, no matter how aggressively the endpoint is hammered.
Handling Distributed State Consistency
Production exposes edge cases that local testing rarely catches. Currently, the Airtable extension processes data in bulk. However, when processing large batches (e.g., 50+ records at once), I noticed occasional state drift between Airtable's network responses and my database updates, resulting in "lost" credits for the user.
This is a classic distributed systems problem. To resolve this, my next architectural step is implementing Idempotency Keys. By ensuring the backend recognizes retried requests from Airtable, the system will guarantee that a user is only charged once per unique bulk action, even if the network stutters.
AI-Augmented Development
I built the majority of this system with the assistance of LLMs (specifically Gemini FAST), eschewing automated coding agents like Cursor.
Using AI as a pair programmer was a massive time-saver for generating boilerplate. However, it also reinforced the need for an experienced human in the loop. The AI frequently suggested sub-par solutions or unnecessary code bloat. A significant portion of my time was spent actively pruning these suggestions and enforcing strict architectural guardrails to keep the Node.js kernel lean and maintainable.
Conclusion & Next Steps
Building this integration proved that the real complexity of modern web apps doesn't lie in connecting APIs, but in handling state, managing trust boundaries, and protecting infrastructure from load and malicious actors.
Looking forward, I plan to expand the platform's capabilities beyond simple text transformation to include direct file processing, offering even more granular control to the end user.
Stack:
- Node.js
- TypeScript
- React
- Tailwind CSS
- Airtable SDK
- Stripe API
- Supabase SDK
- SMTP/DNS