Home Getting Started

Getting Started

Start here.
By Miguel Abascal
8 articles

Quickstart

Quickstart 10 minutes to your first synced transaction. This walks you through registering as a platform on OrangeRails, getting an API key, opening the Connect widget, and pulling your first transaction. What you need before you start - An OrangeRails account (sign up free at orangerails.com) - A wallet, exchange, or payment processor account to connect (Blink, Strike, BTCPay, Bitcoin xpub, or any of the 100+ exchanges) - A web app where you want to embed the Connect widget If you only want to try it as a consumer (not embed in your own app), skip steps 1–3 and just sign in at orangerails.com/app. 1. Get your platform API key In the Orange Rails dashboard, go to Settings → API tokens and click Generate token. Copy the value once; you cannot see it again. The token is a X-Platform-API-Key header you'll send on every call. 2. Decide how your users will connect OrangeRails uses a tiny "subaccount" abstraction so one of your customers maps to one OR subaccount. Either: - Let OR create them on the fly when the user opens the Connect widget (recommended for SaaS apps). - Pre-create them by calling POST /functions/v1/or-provision with the user's identifier in your system. Either way, every API call from your app passes a subaccount_id so OR knows which of your users it's acting on. 3. Drop the Connect widget into your app The widget is a popup served from connect.orangerails.com. Open it with a postMessage handshake: const popup = window.open( "https://connect.orangerails.com/connect?platform=your-platform-slug", "or-connect", "width=480,height=720" ); window.addEventListener("message", (e) => { if (e.origin !== "https://connect.orangerails.com") return; if (e.data.type === "OR_READY") { popup.postMessage( { type: "OR_INIT", platform_api_key: "your-key-here", subaccount_id: "your-customer-uuid", }, "https://connect.orangerails.com" ); } if (e.data.type === "OR_CONNECTION_CREATED") { // user finished connecting; e.data.connection_id is yours } }); The user picks a source (Blink, Kraken, xpub, whatever), enters their credentials, and the widget encrypts those credentials client-side before they ever hit OR. The server stores ciphertext only — we cannot read them. 4. Pull your first transactions Once a connection exists, call sync: curl -X POST https://api.orangerails.com/functions/v1/or-sync \ -H "X-Platform-API-Key: your-key" \ -H "Content-Type: application/json" \ -d '{"subaccount_id":"your-customer-uuid"}' OR fetches from the upstream source, normalizes everything into a single shape, and stores the transactions encrypted-at-rest. You read them via: curl https://api.orangerails.com/functions/v1/or-transactions-list \ -H "X-Platform-API-Key: your-key" \ -d '{"subaccount_id":"your-customer-uuid"}' You get back a JSON array of normalized transactions: id, direction, type, amount_sats or amount, currency, timestamp, counterparty, status, etc. 5. What to read next - API reference — all endpoints, request/response shapes, error codes - How authentication works — the three-layer model (app ↔ OR, OR ↔ source, zero-knowledge wrapper) - Self-hosting guide — run OR on your own infra - Browse all 100+ connections — what sources are supported today Need help? - Something broke → support.orangerails.com - Feature request → feedback.orangerails.com - Found a security issue → see SECURITY.md

Last updated on May 16, 2026

API reference

API reference REST endpoints, webhook events, normalized data shapes. Everything you need to read from OrangeRails programmatically. OrangeRails is an Apache 2.0 connector hub. The hosted API lives at https://api.orangerails.com (which fronts the Supabase Edge Functions at https://lcdicqalreskibdfxkzb.supabase.co/functions/v1/*). Self-hosters point their clients at their own deployment. Authentication Two modes, picked by which header you send: | Mode | Header | Who uses it | |---|---|---| | Platform | X-Platform-API-Key: <hex> + subaccount_id in body | Integrating apps (V3, BitBooks Personal, third-party SaaS) | | Direct | Authorization: Bearer <supabase-jwt> | Consumer users signed in to orangerails.com/app | Full breakdown of how the layers fit together: How authentication works (for integrators). Endpoints All endpoints are POST + JSON unless noted. Catalog (no auth) GET /functions/v1/or-providers Returns the public manifest of every source OrangeRails supports. CDN-friendly, no auth. { "providers": [ { "slug": "blink", "displayName": "Blink", "description": "Lightning + on-chain", "status": "live", "category": "lightning_wallet", "tags": ["live", "lightning"], "popularity": 80, "multiWallet": true, "credentialFields": [ { "name": "api_key", "type": "secret", "label": "Blink API key" } ] } ], "categories": [ { "slug": "exchange", "displayName": "Exchanges", "description": "Crypto exchanges with fiat on/off-ramps", "providerCount": 98 } ] } Connections POST /functions/v1/or-connection-create Body: { subaccount_id, provider_type, encrypted_credentials, label? }. Returns connection_id. The credential ciphertext is opaque to OR — the widget produces it client-side before this call. POST /functions/v1/or-connection-list Body: { subaccount_id }. Returns the user's active connections. POST /functions/v1/or-connection-delete Body: { subaccount_id, connection_id }. Soft-deletes; underlying transactions remain queryable for compliance until explicitly purged. Sync + read POST /functions/v1/or-sync Body: { subaccount_id, connection_id? }. Fetches new transactions from upstream and persists them (encrypted). Idempotent on (connection_id, external_id). POST /functions/v1/or-transactions-list Body: { subaccount_id, connection_id?, since?, limit? }. Returns normalized transactions: { "transactions": [ { "id": "uuid", "external_id": "provider-side-id", "adapter": "blink", "direction": "in", "type": "lightning", "amount_sats": 21000, "currency": "BTC", "description": "Payment from alice", "counterparty": "[email protected]", "status": "settled", "timestamp": "2026-05-14T18:32:00Z" } ], "next_cursor": null } Platform admin POST /functions/v1/or-provision Body: { external_user_id, label? }. Creates (or returns existing) subaccount for the given user identifier. Idempotent. POST /functions/v1/or-platform-display Body: {}. Returns the calling platform's display info (name, logo, allowed return URLs) for the widget header. Stealth Sync endpoints (xpub blind mode) For on-chain Bitcoin connections, OR uses Stealth Sync — the xpub never reaches the server. - POST /functions/v1/or-stealth-connection-create — store a sealed envelope (encrypted xpub) - POST /functions/v1/or-stealth-connection-list - POST /functions/v1/or-stealth-connection-delete - POST /functions/v1/or-stealth-envelope-fetch — retrieve a sealed envelope for browser decryption - POST /functions/v1/or-stealth-transactions-store — store sealed transaction bytes from browser-side BIP 158 filter matching All these speak in opaque bytes. The server cannot decrypt any of them. Errors Every error is a JSON body with error (machine-readable code) and message (human-readable): { "error": "INVALID_PLATFORM_KEY", "message": "Platform API key not recognized" } | HTTP | Error codes | |---|---| | 400 | MISSING_FIELD, INVALID_PROVIDER, INVALID_SUBACCOUNT_FORMAT | | 401 | INVALID_PLATFORM_KEY, INVALID_JWT, EXPIRED_TOKEN | | 403 | SUBACCOUNT_NOT_OWNED, RATE_LIMIT_EXCEEDED | | 404 | SUBACCOUNT_NOT_FOUND, CONNECTION_NOT_FOUND | | 502 | UPSTREAM_PROVIDER_ERROR | Webhooks (planned, Phase 1) Outbound webhook delivery is on the public roadmap for Phase 1 (α). Today, clients poll /or-transactions-list with a cursor. Webhook events under design: - connection.created - connection.deleted - sync.completed - sync.failed Rate limits - Platform mode: 600 calls/minute per platform_id (steady), 100 burst. - Direct mode: 60 calls/minute per user. Rate limit headers on every response: X-RateLimit-Limit: 600 X-RateLimit-Remaining: 597 X-RateLimit-Reset: 1715800000 Exceeded → 429 with RATE_LIMIT_EXCEEDED. SDK Official TypeScript SDK is on the public roadmap for Phase 1. Until then, the API is plain HTTP + JSON — any HTTP client works. A canonical Node.js example client lives at github.com/MorningRevolution/orangerails/tree/dev/examples. Open spec The OpenAPI 3.1 schema is auto-generated from the edge function code and published as part of this repo. Currently v0 (pre-1.0). Once 1.0 ships, the schema is versioned and additive changes only. - OpenAPI spec → github.com/MorningRevolution/orangerails/blob/dev/docs/openapi.yaml (TBD) - Protocol design discussion → OrangeRails Protocol

Last updated on May 16, 2026

Self-hosting guide

Self-hosting guide OrangeRails is Apache 2.0. The hosted service at orangerails.com runs the same code as the public repo — there is no closed enterprise fork. This is the single guide for running OrangeRails on your own infrastructure. Why self-host - Zero trust in OR the company. Even though OR can't decrypt your data (zero-knowledge architecture), some teams need the operator to also be themselves. - Compliance. Regulated entities running their own Supabase or Postgres deployment. - Cost. Skip the hosted billing entirely at any scale. - Hackability. Patch adapters, add private sources, bend the API. What you get with the self-host build Feature parity with hosted, with the exception of: - The hosted Stealth Sync filter CDN (stealth.orangerails.com) and Bitcoin block fetcher (blocks.orangerails.com) — you point your self-host at our public-good CDN or run your own. See below. - SOC 2 attestation (only applies to the hosted service). - The MCP layer (Phase 2 work — both hosted and self-host get it at the same time). Everything else — privacy tiers, ZKA, 100+ source adapters, admin pages, the dashboard — is in the repo. The pieces OrangeRails is a Vite SPA + a Supabase backend (edge functions + Postgres). Two ways to run it: | Approach | When | |---|---| | Supabase Cloud + Cloudflare Pages | Easiest. Matches how we run prod. Free tier covers small deployments. | | Self-host Supabase (or any Postgres) + any static host | Maximum sovereignty. More moving parts. | Pick based on your taste for ops. Both deploy from the same code. Path A — Supabase Cloud + Cloudflare Pages (recommended) 1. Fork the repo gh repo fork MorningRevolution/orangerails --clone=true cd orangerails 2. Create two Supabase projects In the Supabase dashboard, create <yourname>-dev and <yourname>-prod. Note both project refs (xxxxxxxxxxxxxxxxxxxx.supabase.co). 3. Apply migrations Install the Supabase CLI, then: supabase link --project-ref <your-dev-ref> supabase db push Repeat for prod. 4. Set environment variables In your fork, set GitHub Actions repo variables (Settings → Secrets and variables → Actions → Variables): VITE_SUPABASE_PROJECT_ID_DEV = <your-dev-ref> VITE_SUPABASE_URL_DEV = https://<your-dev-ref>.supabase.co VITE_SUPABASE_PUBLISHABLE_KEY_DEV = <your-dev-anon-key> VITE_SUPABASE_PROJECT_ID_PROD = <your-prod-ref> VITE_SUPABASE_URL_PROD = https://<your-prod-ref>.supabase.co VITE_SUPABASE_PUBLISHABLE_KEY_PROD= <your-prod-anon-key> And secrets: CLOUDFLARE_API_TOKEN = <your-cf-token-with-pages-edit> CLOUDFLARE_ACCOUNT_ID = <your-cf-account-id> 5. Create two Cloudflare Pages projects Manually in the CF dashboard (one-time): <yourname>-dev and <yourname>-prod. Don't connect them to Git — the GitHub Actions workflow handles deploys. 6. Push git push origin dev Within ~60 seconds your dev site is live. Promote to prod with: git checkout prod && git merge --ff-only dev && git push origin prod Path B — Self-hosted Supabase + any static host The repo includes a docker-compose.yml to run Supabase locally and a dist/ build that's static HTML/JS — drop it on any web host (nginx, Caddy, S3 + CloudFront, your home server). Quick start git clone https://github.com/MorningRevolution/orangerails cd orangerails docker compose up -d # starts Postgres + Kong + GoTrue + PostgREST + Realtime supabase db push --db-url postgres://... # apply migrations bun install && bun run build # serve dist/ from your static host A reference Caddy config is in caddy/Caddyfile.example. Stealth Sync filter CDN For on-chain xpub connections to work, the browser needs BIP 158 compact block filters. By default, the self-host build fetches them from stealth.orangerails.com (public CDN, no auth, no logs). You can: - Use ours (default, recommended for most). Privacy posture is the same as Sparrow's default behavior with public Esplora. - Run your own. Point your Bitcoin Core node at the filter generator in tools/stealth-filter-server/ and update the env var VITE_STEALTH_FILTER_CDN. Detailed setup: Stealth Sync — Architecture. Upgrades The repo follows the standard dev (staging) and prod (live) branch model. To pick up upstream changes: git remote add upstream [email protected]:MorningRevolution/orangerails.git git fetch upstream git checkout dev git merge upstream/dev git push origin dev # auto-deploys to your dev URL # verify git checkout prod && git merge --ff-only dev && git push origin prod Backup + restore Same as any Postgres / Supabase deployment. The data at rest is ciphertext — backups are similarly opaque. Make sure your age / vault keys are backed up off-server, otherwise a database backup is useless to you. A reference backup script: scripts/backup-vault.sh (writes encrypted dumps to S3-compatible storage). Help - Self-hosting questions → feedback.orangerails.com (tag: self-host) - Security issues → SECURITY.md - General support → support.orangerails.com

Last updated on May 16, 2026

Adapter SDK guide

Adapter SDK guide Build a typed source adapter in a day. This guide walks through writing a new connector for OrangeRails so end users can connect to your service. OrangeRails ships with 102 source adapters out of the box: 4 native (Strike, BTCPay, Blink, Bitcoin xpub via Stealth Sync) plus 98 exchanges via the CCXT bridge. If your service isn't in that list, write an adapter and it will be. What an adapter does An adapter is one TypeScript module that: 1. Declares its slug (the value stored in the connections.provider_type column). 2. Declares its credential shape (what fields the OR Connect widget should render: API key, secret, OAuth token, xpub, etc.). 3. Implements discoverWallets — pure pass-through that returns the wallets the credential can see. No DB writes. 4. Implements syncByWallets — fetches the latest transactions for the wallets the user selected, returns them in the canonical NormalizedTransaction shape. The edge functions handle everything else: auth, persistence, encryption at rest, scheduling, dedup. Your adapter just speaks upstream. The interface // supabase/functions/_shared/providers/types.ts export interface ProviderAdapter { slug: string; displayName: string; description: string; category: 'lightning_wallet' | 'on_chain_wallet' | 'payment_processor' | 'exchange' | 'card' | 'mining' | 'bank' | 'lender'; tags?: string[]; popularity?: number; multiWallet: boolean; credentialFields: CredentialField[]; discoverWallets(credentials: Record<string, unknown>): Promise<DiscoveredWallet[]>; syncByWallets( credentials: Record<string, unknown>, walletIds: string[], cursor: string | undefined, ): Promise<SyncResult>; } NormalizedTransaction (what your adapter returns): { id: string; // provider-side stable id (dedup key) adapter: string; // matches your slug direction: 'in' | 'out'; type: 'lightning' | 'onchain' | 'trade' | 'deposit' | 'withdrawal' | 'fee'; amount_sats?: number; // when source is BTC-denominated amount?: number; // when source is USD/EUR/etc currency?: string; description?: string | null; counterparty?: string | null; status?: string; timestamp: string; // ISO 8601 external_payload?: Record<string, unknown>; // raw provider object, opaque } Walk through: Strike adapter The simplest real adapter in the repo. ~150 lines. supabase/functions/_shared/providers/strike.ts: - Declares slug: 'strike', category: 'payment_processor'. - Credential is a single API key with read-only scopes (partner.account.profile.read + partner.invoice.read) generated at dashboard.strike.me. - discoverWallets returns one synthetic wallet — Strike accounts are single-account, multi-currency. - syncByWallets pages through GET /v1/invoices?$filter=state in ('PAID','PENDING') and created ge '<cursor>' and emits one NormalizedTransaction per PAID/PENDING invoice. UNPAID and CANCELLED skipped. Cursor is the highest created timestamp seen in the batch. Next sync passes it as created ge '<iso>' so Strike does the filtering server-side. Adapters must be self-contained: no DB calls, no platform-auth concerns. The edge functions handle that layer. How to add a new adapter 1. Create the file supabase/functions/_shared/providers/<your-slug>.ts Implement the ProviderAdapter interface. Use strike.ts or btcpay.ts as templates. 2. Register in the dispatch table supabase/functions/_shared/providers/dispatch.ts — import your adapter and add it to the PROVIDERS array: import { yourAdapter } from './<your-slug>.ts'; // ... const PROVIDERS = [ blinkAdapter, xpubAdapter, btcpayAdapter, strikeAdapter, yourAdapter, // ← here ...ccxtAdapters, ]; That's it for registration. All three edge functions (or-providers, or-discover-wallets, or-sync) auto-discover it via the registry. 3. Submit a PR Open against dev in MorningRevolution/orangerails. CI runs bun run build + the providers stress test. Special-case: CCXT-backed exchanges If your target is one of the 100+ exchanges CCXT supports, you don't write an adapter at all. Add an entry to _ccxt-manifest.ts (or regenerate via scripts/generate-ccxt-manifest.mjs) and the CCXT base adapter handles it. Three credential shapes are supported today: apiKey+secret, apiKey+password+secret, apiKey+secret+uid. Exchanges using exotic shapes (DEX wallets, signed messages) need a custom adapter. What's coming - Adapter-side webhook ingestion (today everything is poll-based) - Adapter-side error categorization (so the UI can render "credentials expired" vs "rate limit" differently) - A standalone @orangerails/adapter-sdk npm package so adapters can live in their own repos and OR auto-discovers them from a manifest These are Phase 1 (α) and Phase 2 (β) work. PRs welcome. See also - API reference — the edge functions your adapter plugs into - How authentication works — credential handling end to end - Self-hosting guide — run with your own adapters

Last updated on May 16, 2026

Security and threat model

Security and threat model What we trust, and what we cannot. A plain-English read of the OrangeRails security model — what's in our threat model, what's out, and what assumptions a customer has to accept to use the service. The one-sentence summary OrangeRails holds ciphertext only. We cannot read your financial data, by the architecture, not by promise. A breach of our database leaks opaque bytes, not bank logins or transactions. What we protect against | Threat | How we handle it | |---|---| | Server-side breach (read). An attacker copies our database. | All credentials, transaction memos, counterparty handles, and (on Stealth Sync sources) addresses + amounts are AES-256-GCM encrypted in the user's browser before they reach us. Server-side decryption is impossible without the user's vault password. | | Operator insider abuse. An OrangeRails employee tries to peek at user data. | Same answer. We can't decrypt either. Operator access to the production database doesn't yield plaintext. | | Network MITM. An attacker on the path between user and server. | All traffic is HTTPS with TLS 1.2+ over HTTP/2 or HTTP/3, certs via Let's Encrypt. The Connect widget verifies the parent window's origin before accepting an INIT message. | | Compromised upstream source. Coinbase, Blink, etc., gets breached. | The credential we hold for that source is encrypted at rest, so the attacker can't pivot from our DB to other accounts. But we can't undo the upstream breach itself. | | Replay attacks. An old credential blob is replayed against our edge function. | Credentials are bound to a single user + subaccount via RLS. Replay against a different subaccount fails. | | Plaid-style data monetization. Any business model that requires us to read your data. | Structurally impossible. No analytics on transaction contents. No "anonymized" dataset to sell. The only thing we know about a user is: when they synced, which provider, how many transactions (count only). | What we don't protect against | Threat | Why it's out of scope | |---|---| | Compromised user device. Malware on your machine reads the vault password. | If you lose your machine, you lose. We can't help with endpoint security. | | Compromised vault password. Phished, brute-forced, or shoulder-surfed. | Same. Recovery via Shamir 2-of-3 (Phase 1.5a) limits blast radius but doesn't prevent it. | | Upstream sources collecting their own data. Plaid, Coinbase, Strike see what their own APIs return. | Not our system; we never see what they see, but we also can't stop them from seeing it. | | Statistical analysis of timing or metadata. "User synced 3 times today" leaks behavioral signal. | We log minimum metadata for billing + ops. Documented in the privacy policy. | | Quantum attacks on stored ciphertext. Long-term threat. | PR #20 shipped hybrid X25519 + ML-KEM-768 + ML-DSA-65 for the keys that matter. Per-record AES-256-GCM is still classical; post-quantum work continues in Phase 4. | Cryptographic primitives | Layer | Primitive | |---|---| | Vault password derivation | Argon2id (parameters: 64 MiB memory, 3 iterations, 4 parallelism — tuned to ~1s on a 2026 laptop) | | Master encryption key wrap | Hybrid X25519 + ML-KEM-768 | | Per-record encryption | AES-256-GCM with a fresh 12-byte IV per record | | Co-admin signatures | ML-DSA-65 | | Transport | TLS 1.2+ via Cloudflare Pages | Key files: src/lib/vault.ts (KDF + AES wrappers), src/lib/crypto-fields.ts (per-table helpers). What's in the database, by table | Table | Plaintext columns | Ciphertext columns | |---|---|---| | users | id, email (auth.users) | — | | subaccounts | id, platform_id, external_user_id, created_at | — | | connections | id, subaccount_id, provider_type, created_at | encrypted_credentials, encrypted_label | | transactions | id, connection_id, external_id, timestamp, direction, type, status | encrypted_payload, encrypted_counterparty, encrypted_description | | source_wallets | id, connection_id | encrypted_label | | customer_vault_meta | customer_id, salt, KDF params, KEM/sig public keys | vault_verifier_ciphertext, enc_mek_ciphertext, kem_secret_wrapped, sig_secret_wrapped | | customer_recovery_shares | customer_id, share_index, threshold metadata | share_ciphertext | Operators can see provider types, counts, and timestamps. They cannot see amounts, counterparties, descriptions, or credentials. Stealth Sync (on-chain Bitcoin) specifics Stealth Sync adds an additional layer for xpub-watch sources: - The xpub itself never leaves the browser. It's sealed before storage. - Address derivation happens in WASM inside the browser. - Block filter matching (BIP 158) also happens in the browser; filter files come from a public CDN with no auth and no logs. - Block bodies come from our own Bitcoin Core node, not a third party. - Each transaction OR persists is a sealed envelope; the server cannot tell which addresses appeared in which transactions. See Stealth Sync — Architecture for the full deep dive. Bug bounty + disclosure - Security email: [email protected] - PGP key: published at https://orangerails.com/.well-known/security.txt (Phase 1) - Coordinated disclosure: 90 days. We acknowledge within 48 hours, ship a fix or mitigation within 30 days for high-severity, 90 days otherwise. - Bug bounty program: planned for Phase 2 (β). HackerOne or Bugcrowd, $5,000/quarter initial budget. Don't open public GitHub issues for security vulnerabilities. Email first. Independent reviews - Cryptographic implementation review — planned for Phase 2 (β). Target firms: Trail of Bits, NCC Group, Cure53. Budget $30k–$80k. - SOC 2 Type 1 — planned for Phase 3 (1.0). - SOC 2 Type 2 — planned post-1.0. The point of these reviews is that you don't have to trust our blog post — you get a third-party attestation. Reports will be published with findings + remediations. What customers should do 1. Pick a strong vault password. It's the root of the trust hierarchy. We cannot reset it. 2. Save the Shamir recovery share (PDF/QR) we generate at vault setup. If you lose your password and don't have the recovery share, your encrypted data is gone. 3. Keep your upstream credentials scoped read-only. Strike, Coinbase, etc. let you generate API keys with read-only scopes. Use them. OR doesn't need write access to anything. 4. Self-host if you can't trust the operator. Apache 2.0, run it yourself, eliminate even the residual ops trust.

Last updated on May 16, 2026

Contributing

Contributing OrangeRails is Apache 2.0 open source. PRs welcome, repo conventions and branch model below so your contribution lands smoothly. Branch model Two branches, two URLs. There is no main. dev ← Lovable + agents + PRs land here; auto-deploys to dev.orangerails.com prod ← merged from dev when ready; auto-deploys to orangerails.com (live) Promotion is explicit — a fast-forward merge from dev to prod and a push. Full mental model: Branch flow — dev and prod. PR conventions - Open against dev as the base branch (it's the GitHub default — should auto-select). - One commit per logical change is preferred; the maintainer squashes on merge anyway. - Commit message style: type(scope): subject — see existing git log --oneline for examples. Types we use: feat, fix, chore, docs, ci, copy, refactor, test. - CI must pass before merge (bun run build, type-check, stress test). Setting up locally gh repo clone MorningRevolution/orangerails cd orangerails bun install bun run dev # starts the Vite dev server at localhost:8080 For backend work (edge functions), you'll need a Supabase project. Either: - Use the hosted dev project (ask a maintainer for read access) - Run Supabase locally via supabase start (uses the supabase/config.toml in the repo) Set environment variables in .env: VITE_SUPABASE_URL=https://gposxxmxenrdvewrprle.supabase.co VITE_SUPABASE_PUBLISHABLE_KEY=<your-anon-key> VITE_SUPABASE_PROJECT_ID=gposxxmxenrdvewrprle .env.example has the canonical values to copy. Repo layout src/ routes/ TanStack Router file-based routes components/ Reusable UI components components/landing/ Marketing-page components hooks/ React hooks lib/ Pure utilities (encryption, providers fetch, etc.) integrations/ Supabase client + types stealth/ Stealth Sync widget (postMessage protocol) supabase/ functions/ Edge functions (or-providers, or-sync, or-connection-*, etc.) functions/_shared/ Shared types + adapter framework + sink dispatch migrations/ Versioned SQL (idempotent guards required) scripts/ generate-ccxt-manifest.mjs Regenerates _ccxt-manifest.ts from CCXT stress-test-ccxt.mjs Catches CCXT version drift prerender-plugin.ts Vite plugin for SEO-friendly static HTML Adding a feature Frontend-only 1. Branch from dev: git checkout -b feat/your-feature 2. Write the code under src/ 3. Run bun run build to type-check 4. Open PR against dev Backend-only 1. Edit edge function under supabase/functions/<fn>/index.ts 2. If schema changes, add a migration under supabase/migrations/<timestamp>_<slug>.sql (use idempotent guards: CREATE TABLE IF NOT EXISTS, DROP POLICY IF EXISTS before each CREATE POLICY, etc.) 3. Test locally: supabase functions serve <fn> 4. Open PR New source adapter See the Adapter SDK guide. Short version: implement ProviderAdapter in supabase/functions/_shared/providers/<slug>.ts, register in dispatch.ts, ship. Code style - TypeScript everywhere. No any unless there's a comment explaining why. - Plain English in user-facing copy. No "rotate", "encrypt", "decrypt", "ciphertext" — say "lock", "open the box", "scrambled bytes" instead. - No em-dashes in user-facing copy. Commas, periods, parentheses, or restructure. - No compound-word hyphens in user-facing copy ("customer facing" not "customer-facing"). Hyphens stay in identifiers and slugs. - ESLint runs in CI but is continue-on-error: true — fix warnings if you see them but don't let them block. What we don't accept (yet) - PRs that break zero-knowledge. The architecture rule is server stores ciphertext only for credentials, transaction details, and Stealth Sync data. PRs that move plaintext server-side will be rejected. - PRs that add hard dependencies on a non-free service without a self-host fallback. - PRs that change the public API in breaking ways without a deprecation cycle (additive changes only, per the OpenAPI versioning policy — Phase 1 work). Discussion - Feature requests → feedback.orangerails.com (Fider — vote, propose, track) - Support → support.orangerails.com - Security issues → [email protected] (PGP key at /.well-known/security.txt once Phase 1 ships) - General chat → Telegram + Nostr links in the repo README (Phase 1 work to set up) License Apache 2.0. See LICENSE in the repo root. By contributing, you agree your contribution is licensed under Apache 2.0.

Last updated on May 16, 2026

Open API spec (v0 draft)

Open API spec (v0 draft) OpenAPI 3.1, published and versioned. The machine-readable description of every OrangeRails endpoint, request shape, response shape, and error code. Status: v0 draft We're pre-1.0. The schema is generated from the edge function code but not yet committed to additive-only changes. Breaking changes can happen between v0 and v1. Track the version field at the top of the spec. At 1.0 (Phase 3 on the roadmap), the schema freezes: additive changes only after that. SDK + client generation become safe. Where to find it - OpenAPI 3.1 YAML — github.com/MorningRevolution/orangerails/blob/dev/docs/openapi.yaml (TBD — will land in Phase 1 once the spec generator is wired in CI) - Interactive playground — docs.orangerails.com/openapi (planned for Phase 4 when the dedicated dev portal lands) - Today: the API reference is the human-readable version, kept in sync by hand What it covers The spec enumerates every endpoint under /functions/v1/*, both the public catalog and the platform/direct-auth endpoints: - or-providers — public catalog of source adapters + categories - or-platform-display — platform display info for the Connect widget - or-provision — create or fetch a subaccount for a platform user - or-connection-create, or-connection-list, or-connection-delete — connection lifecycle - or-discover-wallets, or-source-wallets-set — wallet selection inside a connection - or-sync, or-transactions-list — fetch + read normalized transactions - or-stealth-connection-create, -list, -delete, or-stealth-envelope-fetch, or-stealth-transactions-store — Stealth Sync sealed envelope endpoints Each operation declares its request body schema, response schema, error codes, rate limit headers, and auth requirements. Schemas worth knowing ProviderManifest type: object required: [slug, displayName, status, multiWallet, credentialFields] properties: slug: { type: string } displayName: { type: string } description: { type: string } status: { type: string, enum: [live, beta, coming_soon] } category: { type: string, enum: [lightning_wallet, on_chain_wallet, payment_processor, exchange, card, mining, bank, lender] } tags: { type: array, items: { type: string } } popularity: { type: integer } multiWallet: { type: boolean } credentialFields: type: array items: type: object required: [name, type, label] properties: name: { type: string } type: { type: string, enum: [string, secret] } label: { type: string } placeholder: { type: string } NormalizedTransaction type: object required: [id, adapter, direction, type, timestamp] properties: id: { type: string, description: "Provider-side stable id (dedup key)" } external_id: { type: string } adapter: { type: string } direction: { type: string, enum: [in, out] } type: { type: string, enum: [lightning, onchain, trade, deposit, withdrawal, fee] } amount_sats: { type: integer } amount: { type: number } currency: { type: string } description: { type: string, nullable: true } counterparty: { type: string, nullable: true } status: { type: string } timestamp: { type: string, format: date-time } external_payload: { type: object, additionalProperties: true } Error envelope type: object required: [error, message] properties: error: { type: string, description: "Machine-readable code (e.g. INVALID_PLATFORM_KEY)" } message: { type: string, description: "Human-readable description" } Versioning policy (post-1.0) - MAJOR.MINOR.PATCH in the spec info.version field. - MAJOR bump = breaking change. Only allowed in a deprecation cycle: announce in newsletter + GitHub release, ship v(N) alongside v(N+1) for 90 days, then sunset. - MINOR bump = additive (new endpoint, new optional field, new enum value). - PATCH bump = doc-only. A v1 endpoint will be served under /functions/v1/*. When v2 ships, it lives at /functions/v2/* and v1 keeps working for the deprecation window. Generators + tooling These will be officially blessed once the spec lands: - TypeScript SDK — auto-generated from the spec, published as @orangerails/sdk (planned Phase 1) - Postman collection — exported from the spec, committed under docs/postman.json - MCP-compatible tool definitions — derived from the same spec so AI assistants can call the API with full type safety (Phase 3 — MCP-1.0 spec lock) What's NOT in the spec - The Stealth Sync postMessage protocol — that's a browser-to-widget contract, not an HTTP API. Documented separately in src/stealth/lib/postmessage.ts and on the Stealth Sync wiki page. - Internal admin endpoints (/admin/*, /portal/*) — not part of the public surface. - The encryption details (which fields are sealed where) — those live in the Security and threat model doc, because they're architectural, not API-surface. See also - API reference — the human-readable spec - Quickstart — get a transaction syncing in 10 minutes - Adapter SDK guide — write your own adapter against the canonical types

Last updated on May 16, 2026