Legal
Security & Architecture
Effective date: May 28, 2026 · ZONTIK LLC · Wyoming, United States
This document describes how WheelRunner handles brokerage credentials, user data, and our planned Alpaca OAuth integration. It is intended for partners reviewing our security posture (including Alpaca's OAuth review team), independent auditors, and any user who wants to understand how their data is protected.
1. Product Summary
WheelRunner (wheelrunner.app) is a single-strategy options-trading SaaS that helps individual investors execute the wheel options strategy (cash-secured puts → assignment → covered calls). The product is operated by ZONTIK LLC, a Wyoming limited liability company with registered office at 1309 Coffeen Ave, Ste 1200, Sheridan, WY 82801, United States.
WheelRunner is a technology platform, not a registered investment adviser or broker-dealer. All trades are executed against the user's own Alpaca brokerage account based on rules the user configures.
2. Authentication and Identity
| Layer | Mechanism |
|---|---|
| End-user sign-in | Supabase Auth (email/password + Google OAuth). Passwords are bcrypt-hashed by Supabase; we never see plaintext. |
| Session transport | HttpOnly, Secure, SameSite=Lax cookies. JWTs with short expiry and automatic refresh. |
| Server-to-server | Supabase service role key (server-only env var), never exposed to client bundles. |
| Cron / scheduled jobs | Authenticated via Authorization: Bearer ${CRON_SECRET}; unauthenticated requests are rejected with HTTP 401. |
3. Brokerage Credential Handling
3.1 Today — API-key integration (transitional)
Until our Alpaca OAuth integration is approved, users supply an Alpaca-issued API key + secret on the in-app Connect screen. Those credentials are handled as follows:
- Transport: HTTPS only (TLS 1.3) between the user's browser and our Next.js server. Connect requests are processed entirely server-side.
- At-rest encryption: Both API key and secret are encrypted with AES-256-GCM authenticated encryption before being written to the alpaca_connections Postgres table (Supabase). Each row uses a fresh 96-bit random IV and a 128-bit authentication tag. Ciphertext is stored with an explicit version prefix (enc:v1:) so the encryption scheme can be rotated without breaking historic rows.
- Key custody: The encryption key (TOKEN_ENCRYPTION_KEY) is a 32-byte (256-bit) random value held in Vercel environment variables as a sensitive secret. It is accessible only to server-side code, never logged, never returned in API responses, and never shipped to the client bundle.
- In-memory lifetime: Plaintext credentials exist only during a single server request — decrypted on read, passed to the relevant Alpaca call, and discarded when the request returns. There is no process-level caching of plaintext credentials.
- Logging: Plaintext credentials are never logged. Error paths log structural metadata only. Sentry breadcrumbs exclude credential columns.
- Browser exposure: Decrypted credentials are never serialised into HTML, JSON responses, or client-side bundles. All Alpaca calls originate from our server.
- User-controlled disconnect: The user can disconnect at any time, which removes the row from alpaca_connections and revokes our ability to call Alpaca on their behalf.
3.2 Target — OAuth (post-approval)
Once Alpaca approves our OAuth integration, the credential model changes as follows. The data flow, encryption-at-rest scheme, and disconnect semantics remain identical — only the credential type changes.
- Authorisation flow: Standard OAuth 2.0 authorisation code grant. The user is redirected to Alpaca's authorisation endpoint, approves the requested scopes, and is redirected back to our callback URL with a one-time authorisation code.
- State parameter: Cryptographically random per request, stored server-side keyed to the user's session, validated on callback to prevent CSRF. Single-use.
- Token exchange: Server-side using our Alpaca client credentials (held as server-only env vars). Access tokens, refresh tokens, and expiry are written using the same AES-256-GCM scheme described above.
- Scopes: account:write (place + cancel orders), account:read (positions, orders, activities, account snapshot), data (market data needed for the wheel scanner). We will request no broader scope than the product needs.
- Refresh: Tokens are refreshed proactively when within 5 minutes of expiry, or reactively on a 401 from Alpaca, whichever comes first. Refresh failures invalidate the connection and surface a re-authorise prompt to the user.
- Disconnect: Disconnect revokes the token at Alpaca's revocation endpoint and removes the row from our database. Disconnect at Alpaca's dashboard also takes effect — our next call will 401 and we surface the re-authorise prompt.
4. Database Security
- Hosting: Supabase (PostgreSQL) in a private project, accessible only over TLS.
- Row-Level Security (RLS): Enabled on every user-data table. Each policy scopes reads and writes to auth.uid() = user_id. End-user sessions (anon key) cannot read another user's rows.
- Service role: Used only in server-side code (Next.js Route Handlers, Server Actions, Cron). The service role key is never shipped to the browser. Sensitive write operations include an explicit user_id filter in every query.
- Schema mandates: Every new table requires GRANT ALL TO service_role, authenticated to satisfy Supabase's October-2026 default-grant removal — enforced by code review and documented in the migration template.
5. Application Architecture
- Hosting: Vercel (serverless Node 20 runtime).
- Code: Next.js 14 (App Router). All Alpaca calls are made from server-side Route Handlers, Server Components, or cron jobs — never from client components.
- Secrets management: All secrets (Supabase service role key, Alpaca client credentials, Stripe webhook secret, encryption key, cron secret, Anthropic API key, Finnhub API key) are Vercel environment variables, marked sensitive and only available at runtime to the server bundle.
- Network egress: Outbound calls go only to Alpaca, Supabase, Stripe, Anthropic, Finnhub, Resend, PostHog, and Sentry. No other third-party network sinks.
6. Sub-processors
We share data only with the following sub-processors, each bound by their own DPA and privacy policy. The same list is mirrored in our Privacy Policy.
| Provider | Purpose | Data shared |
|---|---|---|
| Supabase | Database & Auth | All structured user data |
| Alpaca Markets | Brokerage API | Encrypted brokerage credentials, order requests, account/position data |
| Stripe | Payments | Email, subscription plan |
| Anthropic (Claude) | AI Mentor Chat | Chat messages, position context |
| Vercel | Hosting | HTTP requests, IP addresses |
| Posthog | Analytics | Anonymised usage events |
| Sentry | Error monitoring | Error traces (no financial data, no credentials) |
| Resend | Transactional email | Email address |
| Finnhub | Earnings calendar | Ticker symbols only |
7. Incident Response
- Detection: Sentry captures unhandled exceptions and abnormal HTTP error rates. Supabase and Vercel logs are retained 30 days for security investigation.
- Containment: We can revoke any user's Alpaca connection by deleting their row in alpaca_connections. We can rotate the encryption key and force re-authorisation across all users by deploying with a new key and running a backfill.
- Notification: In the event of a security incident that may expose user data, we will notify affected users by email within 72 hours and post a status notice at wheelrunner.app.
- Vulnerability reports: Reports to info@zontik.co are acknowledged within one business day. Critical vulnerabilities are patched within 7 days. We will not pursue legal action against good-faith security researchers.
8. Key Rotation and Audit
- TOKEN_ENCRYPTION_KEY: Rotatable. The enc:v<n>: version prefix on stored ciphertext lets us run a backfill (decrypt with v1, re-encrypt with v2) while retaining read compatibility during the transition window.
- Supabase service role key: Rotatable via the Supabase dashboard; deploy required to pick up the new value.
- Alpaca client credentials: Rotatable via Alpaca's developer dashboard.
- Audit log: Cron-triggered reconciliation writes a per-user record of inserts and updates. Administrative writes to alpaca_connections are restricted to the user-initiated connect route and the lazy-encryption migration path (idempotent, structural — no value change beyond ciphertext re-encoding).
9. What WheelRunner Does NOT Do
- We do not initiate withdrawals, transfers, or non-trading account actions against any user's Alpaca account.
- We do not store Alpaca usernames, account passwords, or PINs.
- We do not share user data with advertisers, data brokers, or any third party outside the sub-processor list in Section 6.
- We do not run training jobs on user trading data for any AI model. AI Mentor chat is a per-request inference call to Anthropic's hosted Claude API with no fine-tuning, no training-data inclusion, and chat history is auto-deleted after 12 months.
- We do not retain plaintext brokerage credentials in any cache, backup, or log.
10. Document Changes
Material changes to this document will be re-versioned with the effective date at the top updated. Sub-processor additions are reflected here and in the Privacy Policy simultaneously.
11. Contact
For security questions or to report a vulnerability:
ZONTIK LLC — Security
Email: info@zontik.co
Registered Office: 1309 Coffeen Ave, Ste 1200, Sheridan, WY 82801, United States
State of Incorporation: Wyoming, United States