Security

Security isn't a feature.
It's the architecture.

Every design decision in Flowstate starts with data minimization. If we don't need to store it, we don't. If we can process it in memory, we do.

Zero bytes retained
TLS everywhere
Timing-safe auth
In-memory processing
Railway infrastructure

Ghost Protocol

Ghost Protocol is Flowstate's zero-retention architecture. Documents are processed entirely in memory and cryptographically wiped on every exit path — success, failure, or exception.

🧠
In-Memory Processing Only
PDF attachments are read into BytesIO buffers — Python's in-memory file-like objects. They are never written to Railway's filesystem, never uploaded to S3 or any object store, and never passed to any system that persists data.
# Attachment extracted from multipart form
pdf_bytes = BytesIO(attachment.read())
# Never: open('file.pdf', 'wb').write()
🔐
Cryptographic Wipe on Every Exit
Every code path — successful extraction, validation failure, unhandled exception, or timeout — ends with explicit buffer deletion and a garbage collection call. There is no path through the code that leaves document data in memory.
finally:
  pdf_bytes.close()
  del pdf_bytes
  gc.collect()
  # Runs on success AND exception
📊
Metadata-Only Logging
Flowstate logs event metadata to PostgreSQL: the sender email address, timestamp, processing duration, validation status, and ERP HTTP status. The document contents, extracted fields, and vendor data are never written to the database.
# What IS stored in the audit log:
"sender_email", "status", "duration_ms"
# What is NOT stored:
# vendor_name, line_items, weights, PO
gc.collect() After Every Request
Python's garbage collector doesn't always reclaim memory immediately. Flowstate explicitly calls gc.collect() after each document processing cycle to ensure that buffer references are cleared from all Python object generations, not just reference-counted objects.
import gc
# Force all three GC generations
gc.collect(0# gen 0 — youngest
gc.collect()  # all generations

Data Flow Architecture

Exactly where your data lives at every stage — and when it's destroyed. Every step is deterministic and auditable.

Document Lifecycle — Where Data Lives and When It Dies
Stage
Location
Persisted?
Who can see it?
Destroyed when?
Email received
SendGrid transit buffer
No
SendGrid (transit only)
On webhook delivery
PDF attachment
Python BytesIO (RAM)
Never
Flowstate process only
After extraction call
Gemini API call
TLS-encrypted transit
Not by Flowstate
Google Gemini (transient)
After response received
Extracted JSON
Python dict (RAM)
Never
Flowstate process only
After ERP push + gc()
Event metadata
PostgreSQL (Railway)
Yes (metadata only)
Dashboard + API key holders
Per plan retention policy
Clean JSON (ERP)
Your ERP system
By your ERP
Your team only
Per your retention policy

Infrastructure

Flowstate runs on Railway's managed infrastructure with strict environment variable isolation and HTTPS-only access at every layer.

🚂
Railway Hosting
Deployed on Railway's managed container platform. Automatic TLS termination, container isolation, and zero-downtime deploys. The production service runs in Railway's Singapore region.
🔒
HTTPS Everywhere
All traffic to and from Flowstate is TLS-encrypted. Railway enforces HTTPS at the edge. Internal service-to-service calls (to Gemini, to ERP webhooks) use HTTPS only — no plaintext HTTP permitted.
🌍
Environment Variable Secrets
All credentials (API keys, database URL, Slack webhook) are stored in Railway's encrypted environment variable store. No secrets are hardcoded in source code or committed to version control.
🗄️
PostgreSQL (Railway-Managed)
The audit log database is a Railway-managed PostgreSQL instance. The DATABASE_URL is injected at runtime via environment variable. No direct database port is exposed to the internet.
📧
SendGrid Inbound Parse
Email inbound processing uses SendGrid's Inbound Parse webhook. MX records point mail.flowstatelogistics.net to mx.sendgrid.net. DKIM and DMARC records are configured for the sending domain.
⚙️
Stateless Architecture
Flowstate is a stateless service — each request is processed completely independently. No server-side sessions, no request state carryover, no in-process caches containing customer data.

API Security

Every authenticated endpoint uses timing-safe comparison, preventing timing oracle attacks. The webhook endpoint uses a separate bypass mechanism that doesn't leak key validity through response timing.

🔑 X-API-Key Authentication
All API endpoints (except the SendGrid webhook when SENDGRID_INBOUND_OPEN=true) require an X-API-Key header. Keys are compared using hmac.compare_digest — a constant-time function that prevents timing-based key enumeration.
import hmac, secrets
# Timing-safe comparison
valid = hmac.compare_digest(
  provided_key,
  settings.API_SECRET
)
🚦 Webhook Auth Bypass
The SendGrid inbound webhook endpoint requires no API key when SENDGRID_INBOUND_OPEN=true — because SendGrid's inbound parser doesn't send credentials. This is an explicit, deliberate configuration, not a security hole. All other endpoints remain protected.
# SENDGRID_INBOUND_OPEN=true
# Only bypasses /webhooks/sendgrid
# All /admin/* endpoints: still require key
# All /api/* endpoints: still require key
📏 Payload Size Limits
Incoming PDF attachments are checked against a configurable MAX_UPLOAD_BYTES limit before processing begins. Oversized payloads are rejected with a 413 response before any AI API calls are made — preventing abuse and cost overruns.
# Default: 20MB per attachment
if len(pdf_bytes) > MAX_UPLOAD_BYTES:
  raise PayloadTooLargeError()
🚫 Error Response Sanitization
Flowstate uses a global exception handler that prevents stack traces, internal error messages, or system information from leaking in API responses. All 500 errors return a generic message. All 4xx errors return only the code and category — never implementation details.
# Exception handler in main.py
# 500: {"error": "internal_error"}
# 401: {"error": "authentication_failed"}
# No stack traces. Ever.

What we don't do

Sometimes the most important security guarantee is a clear statement of what a system explicitly doesn't do. Here's ours.

🚫
We don't store PDFs
PDF attachments are never written to any storage system — not Railway's filesystem, not S3, not a database column. They exist only in RAM for the duration of processing and are garbage-collected immediately after.
🚫
We don't log document contents
Extracted data — vendor names, PO numbers, line items, weights, prices — is never written to our database or logs. Our audit log contains only metadata: sender, timestamp, status, and duration.
🚫
We don't share data with third parties
Document content is sent to exactly one third party: Google Gemini, for AI extraction. That's it. No analytics platforms, no data brokers, no marketing tools. Flowstate has no tracking pixels or third-party scripts.
🚫
We don't hardcode credentials
All API keys, database URLs, and secrets are stored in Railway's environment variable store. The source code contains no hardcoded credentials, no .env files committed to git, and no fallback secrets.
🚫
We don't train on your data
Flowstate uses Google Gemini's API — not a fine-tuned model trained on customer data. Your documents are sent to Gemini's standard API endpoint and are subject to Google's standard API data handling policy, not any Flowstate training process.
🚫
We don't expose raw errors
Exception details, stack traces, and internal system messages are never exposed in API responses or error pages. All errors are sanitized through a global exception handler before reaching any client.

Have a specific security question?

We're happy to go deeper on any aspect of the architecture. Email us or book a call with a technical walkthrough.