Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
23 KiB
System Architecture
Last updated: 2026-04-17 (15 Docker containers + k3s SHKeeper pods + Windows DocServer VM + dev stack + crypto treasury + foreign qualification + compliance check tool)
Overview
Performance West runs as a multi-service stack on a Debian 13 VM (207.174.124.71):
- Astro static site — marketing pages, service descriptions, CRTC order form, crypto payment page
- Express API — order creation, Stripe/PayPal/SHKeeper checkout, identity verification, webhook receiver, portal setup API
- ERPNext — CRM, orders, invoicing, ticketing, customer portal (v15 + MariaDB, custom image with 4 Frappe apps). Portal at
portal.performancewest.net - Listmonk — email marketing at
lists.performancewest.net(Go binary, PostgreSQL-backed). 22 campaigns across 4 lists. Bounce processing via POP3 from Carbonio. - MinIO — S3-compatible document storage (formation docs, compliance reports, binder PDFs)
- Ollama — local LLM for document generation and email drafting (qwen2.5:7b)
- Workers — Python automation: Anytime Mailbox signup (Playwright + IMAP OTP), BC incorporation, Flowroute DID provisioning, document generation, AMB location scraping, payment reminders, GCKey provisioning (Playwright), compliance calendar renewal lifecycle, client email processing
- SHKeeper — self-hosted crypto payment processor via k3s (BTC, LTC, DOGE, ETH, BNB, MATIC, TRX). TRX uses nginx proxy at :5555 → TronGrid with API key injection. ETH uses proxy at :5556 → publicnode.
- Crypto Treasury — post-payment pipeline: SHKeeper → Bridge (Stripe) offramp → RelayFi bank → Relay debit card → vendor payments. Manual mode until Bridge approval.
- Umami — self-hosted web analytics at
analytics.performancewest.net - PostgreSQL — API state, orders, identity verifications, AMB locations, sessions, jurisdictions, foreign qualifications, crypto ledger (api-postgres; also hosts Listmonk DB). 67 migrations applied.
- Dev stack — separate environment at
dev.performancewest.net/api.dev.performancewest.net(port 4323/3002, own PG on 5433). Systemd unit:performancewest-dev.service.
Payment Gateway Architecture
Active gateways: Stripe (card/ACH/Klarna), PayPal Direct (Orders API v2), SHKeeper (crypto).
| Gateway | Status | Methods | Surcharge |
|---|---|---|---|
| Stripe | Active | Card (+3%), ACH (0%), Klarna (+6%) | Via Stripe Checkout Sessions |
| PayPal | Active | PayPal Direct (+3%) | Orders API v2 with capture/tracking/refund |
| SHKeeper | Active | Crypto (0%) — BTC/LTC/DOGE + EVM chains | Branded /order/crypto-pay page with coin selector |
| Adyen | Future | Card/ACH/Klarna/CashApp/AmazonPay (frappe_adyen installed, not configured) |
Payment card routing for vendor expenses:
| Customer paid with | Filing card | ERPNext SID | Funds timing |
|---|---|---|---|
| Card/ACH/Klarna | Stripe Issuing virtual card | SID-0002 | T+2 card, T+4 ACH |
| PayPal | PayPal Mastercard | SID-0001 | Instant |
| Crypto | crypto-filing-card | crypto-filing-card | Manual |
Fund availability detection: Stripe balance.available webhook checks settlement timing (T+2 card/Klarna, T+4 ACH). PayPal is instant. Crypto is manual. When funds clear: Stripe Issuing topup → advance to "Client Selection" → email client portal setup link.
ERPNext custom Frappe apps (baked into performancewest-erpnext:latest):
| App | Purpose |
|---|---|
frappe_crypto |
SHKeeper crypto gateway |
frappe_adyen |
Adyen gateway (future) |
frappe_ca_registry |
BC Corporate Online incorporation automation |
performancewest_erpnext |
Surcharge hooks, identity gate, custom DocTypes |
Service Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Proxmox Host │
│ │
│ ┌── Linux VM (Debian 13) — 207.174.124.71 ───────────────┐ │
│ │ │ │
│ │ Docker Compose (15 containers) │ │
│ │ ├── site (Astro → nginx:alpine) :4322 │ │
│ │ ├── api (Express/TypeScript) :3001 │ │
│ │ ├── api-postgres (app data) :5432 │ │
│ │ ├── erpnext (custom image + 4 apps) :8080 │ │
│ │ ├── erpnext-worker-default │ │
│ │ ├── erpnext-worker-short │ │
│ │ ├── erpnext-scheduler │ │
│ │ ├── erpnext-mariadb :3306 │ │
│ │ ├── erpnext-redis :6379 │ │
│ │ ├── listmonk (email marketing) :9100 │ │
│ │ ├── minio (document storage) :9000/:9001 │ │
│ │ ├── workers (Python automation) :8090 │ │
│ │ ├── ollama (local LLM) :11434 │ │
│ │ ├── umami (analytics) :3100 │ │
│ │ └── umami-postgres │ │
│ │ │ │
│ │ Dev Stack (docker compose at /opt/performancewest-dev) │ │
│ │ ├── dev-site (Astro → nginx:alpine) :4323 │ │
│ │ ├── dev-api (Express/TypeScript) :3002 │ │
│ │ ├── dev-api-postgres :5433 │ │
│ │ └── dev-workers (Python automation) │ │
│ │ │ │
│ │ k3s / Kubernetes (SHKeeper crypto payments) │ │
│ │ ├── shkeeper-deployment (Flask API) NodePort :30723 │ │
│ │ ├── bitcoin-shkeeper (3 containers) │ │
│ │ ├── ethereum-shkeeper (3 containers) via :5556 proxy │ │
│ │ ├── polygon-shkeeper (3 containers) │ │
│ │ ├── bnb-shkeeper (3 containers) │ │
│ │ ├── tron-shkeeper (3 containers) via :5555 proxy │ │
│ │ ├── litecoin-shkeeper (3 containers) │ │
│ │ ├── dogecoin-shkeeper (3 containers) │ │
│ │ └── mariadb (SHKeeper database) │ │
│ │ │ │
│ │ nginx RPC Proxies (inject API keys, strip basic auth) │ │
│ │ ├── :5555 → api.trongrid.io + TRON-PRO-API-KEY │ │
│ │ └── :5556 → ethereum-rpc.publicnode.com │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌── Windows VM (DocServer) — 108.181.102.34 ─────────────┐ │
│ │ SSH: port 22422 (key auth) │ │
│ │ Office 365 Word (COM automation) │ │
│ │ Python 3.13 + pywin32 + minio SDK │ │
│ │ docserver_worker.py (MinIO poller, 12s interval) │ │
│ │ Task Scheduler: PW-DocserverWorker (AtLogOn) │ │
│ │ Auto-logon configured (requires RDP after cold reboot) │ │
│ │ Private network: 10.4.20.247 → MinIO via nginx │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
DNS
| Domain | Target |
|---|---|
performancewest.net |
site (:4322) |
api.performancewest.net |
api (:3001) |
crm.performancewest.net |
ERPNext (:8080) |
lists.performancewest.net |
Listmonk (:9100) — email marketing |
portal.performancewest.net |
ERPNext (:8080) — client portal |
analytics.performancewest.net |
Umami (:3100) |
pay.performancewest.net |
SHKeeper API (:5000 via k3s NodePort :30723) |
crypto.performancewest.net |
SHKeeper admin UI (:30723 via nginx) |
dev.performancewest.net |
Dev site (:4323) |
api.dev.performancewest.net |
Dev API (:3002) |
minio.performancewest.net |
MinIO S3 API (:9000) |
minio-console.performancewest.net |
MinIO Console (:9001) |
All A records point to 207.174.124.71. TLS via Let's Encrypt (certbot, 8 certs). Mail records unchanged (HestiaCP at 207.174.124.15).
External panels:
| Domain | Service |
|---|---|
cp.carrierone.com |
HestiaCP — DNS, .ca domain/email provisioning |
Data Flow
Browser
│
├── performancewest.net ──────> nginx ──> site (static pages)
│
├── api.performancewest.net ──> nginx ──> Express API
│ ├── api-postgres (state fees, API keys)
│ ├── ERPNext REST API (CRM, orders, tickets)
│ ├── Listmonk API (email marketing)
│ └── Workers HTTP API (job dispatch)
│
├── crm.performancewest.net ──> nginx ──> ERPNext
│ ├── erpnext-mariadb
│ └── erpnext-redis
│
├── pay.performancewest.net ──> nginx ──> SHKeeper (k3s :5000)
│
├── lists.performancewest.net ──> nginx ──> Listmonk (:9100)
│
├── portal.performancewest.net ──> nginx ──> ERPNext (:8080)
│
├── minio.performancewest.net ──> nginx ──> MinIO S3 (:9000)
└── minio-console.performancewest.net ──> nginx ──> MinIO Console (:9001)
Order Flow (ERPNext BPM)
Customer places order → Express API → ERPNext Sales Invoice + Payment Request
→ ERPNext payment gateway (Adyen or SHKeeper) → Customer pays
→ ERPNext webhook → Express API → Workers job server
→ Workers execute: name search → filing → EIN → doc gen
→ Documents uploaded to MinIO
→ ERPNext status → Review → Admin approves → Delivered
Key Integration Points
| From | To | Method | Purpose |
|---|---|---|---|
| Website | Express API | HTTPS | Forms, orders, name search |
| Express API | ERPNext | REST API | CRM, orders, invoicing, tickets |
| Express API | Workers | HTTP :8090 | Job dispatch (via webhooks) |
| Express API | Listmonk | REST API | Subscriber onboarding, email campaigns |
| ERPNext | Express API | Webhooks | Workflow state change notifications |
| ERPNext | Adyen | Sessions API v71 | Card/ACH/Klarna/CashApp/AmazonPay (via frappe_adyen) |
| ERPNext | SHKeeper | REST API :5000 | Crypto payment requests (via frappe_crypto) |
| Workers | ERPNext | REST API | Status updates, workflow advances |
| Workers | MinIO | S3 API | Document upload/download |
| Workers | Ollama | HTTP :11434 | LLM doc generation, email drafting |
| Workers | DocServer | MinIO transport | DOCX-to-PDF conversion (MinIO poller: to-convert/ → converted/) |
| Workers | State portals | Playwright | Name search, entity filing |
| Workers | IRS | Playwright | EIN obtainment |
| ERPNext | SMTP | Invoices, notifications, ticket replies | |
| Relay debit card | State portals | Playwright | Filing fee payment |
| Workers | HestiaCP | SSH | .ca domain/email provisioning (cp.carrierone.com) |
| Workers | Flowroute | REST API | Canadian DID provisioning |
| Workers | Porkbun | REST API | .ca domain registration |
| Workers | Client mailboxes | IMAP | Monitor/process client .ca email |
| Website | Anytime Mailbox | External | Vancouver mailbox for Canadian carrier clients |
| ERPNext | Venn.ca | Referral | Canadian business banking referral |
| AI Agents | MCP Server | stdio | Service lookup, pricing, tools (npm package) |
Worker Processes (Python)
The workers container runs a job server (job_server.py on port 8090) that dispatches jobs
to specialized worker modules. Jobs are triggered by ERPNext webhooks via the Express API.
| Worker | File | Trigger | Purpose |
|---|---|---|---|
| Job Server | job_server.py |
HTTP POST :8090 | Central dispatcher — 22 job handlers |
| CRTC Pipeline | services/canada_crtc.py |
Webhook | 14-step CRTC carrier registration pipeline |
| GCKey Provisioner | gckey_provisioner.py |
Called by CRTC Step 11 | Playwright-based GCKey account creation (5-step wizard, hCaptcha) |
| Renewal Worker | renewal_worker.py |
Daily cron (7 AM) | Compliance calendar lifecycle: upcoming → due soon → invoice → paid → completed → re-calendar |
| Formation Worker | formation_worker.py |
Webhook | US state LLC/Corp filing via Playwright |
| Binder Compiler | binder_compiler.py |
Called by pipeline | PDF merge: certificate + articles + CRTC letter + OA |
| Document Gen | document_gen/ |
Called by pipeline | DOCX template fill → MinIO → DocServer PDF conversion |
| HestiaCP Provisioner | hestia_provisioner.py |
Called by pipeline | .ca domain + 14 mailboxes via SSH to cp.carrierone.com |
| Client Email Processor | client_email_processor.py |
Cron (15 min) | IMAP monitor for client .ca mailboxes |
| AMB Scraper | amb_location_scraper.py |
Daily cron | Anytime Mailbox location pricing + sold-out detection |
| Payment Reminder | payment_reminder.py |
Cron (5 min) | Abandoned cart recovery (15min/1d/2d intervals) |
| Commission Worker | commission_worker.py |
Cron (daily 02:00) | Agent commission eligibility check (14-day holdback) |
| Crypto Payment Worker | crypto_payment_worker.py |
Cron (60s) | Treasury state machine: received → sizing → offramping → funds_at_relay → settled |
| Cold Wallet Sweeper | cold_wallet_sweeper.py |
Cron (30 min) | Sweep SHKeeper hot wallet excess to cold storage |
| Relay Deposit Monitor | relay_deposit_monitor.py |
Cron (5 min) | IMAP parser for Relay bank deposit alerts |
| USF Factor Monitor | usf_factor_monitor.py |
Cron (daily 09:00 CT) | Scrape USAC quarterly USF factor, email all FCC carriers |
| De Minimis Check | deminimis_factor_check.py |
Cron (daily 03:00) | Alert if fcc_deminimis_factors missing for current/next year |
| CDR Retention Sweeper | cdr_retention_sweeper.py |
Cron (daily 05:00) | Purge CDR data past retention window |
| CDR Unlock Nudge | cdr_unlock_nudge.py |
Cron (daily 10:00 CT) | Email customers with locked CDR studies behind paywall |
| FCC RMD Scraper | fcc_rmd_removed_scraper.py |
Cron (weekly Wed) | Track FCC RMD carrier removals |
| Foreign Qual Handler | services/foreign_qualification.py |
Webhook | Multi-state Certificate of Authority filings |
| New Carrier Bundle | services/new_carrier_bundle.py |
Webhook | 6-handler chain: CORES → DC Agent → 499 Init → RMD → CPNI → CALEA |
CRTC Pipeline Steps (14 total)
| Step | Name | Key Actions |
|---|---|---|
| 1 | Order Received | Validate, create ERPNext Sales Order |
| 2 | Payment Confirmed | Create Sales Invoice + Payment Entry |
| 3 | Client Selection | Client picks mailbox unit, DID, domain |
| 4 | BC Incorporation | Playwright → BC Corporate Online |
| 5 | Domain Registration | Porkbun .ca + HestiaCP provisioning |
| 6a | CRTC Letter Generation | DOCX template → MinIO → DocServer PDF |
| 6b | eSign | Email JWT link → client signs → resume pipeline |
| 7 | CRTC Submission | Mail signed letter to Secretary General |
| 8 | Anytime Mailbox | Playwright signup + IMAP OTP |
| 9 | Binder Compilation | PDF merge of all documents |
| 10 | Delivery | Email binder + admin print/ship email |
| 11 | BITS Registration | GCKey provisioning + admin ToDo for BITS filing |
| 12 | CCTS Membership | Admin ToDo + client obligations email |
| 13 | Compliance Calendar | Create 17 compliance entries (regulatory + tax + ATS) |
| 14 | Ready for Review | Final admin review before marking Delivered |
Compliance Calendar Renewal Lifecycle
The renewal_worker.py runs daily at 7 AM and manages the full lifecycle of recurring
compliance obligations for all carriers:
Upcoming → Due Soon (30 days out) → Invoice Sent → Paid → Completed → auto-re-calendar next year
- 17 entries per carrier: BC annual report, CRTC annual maintenance, mailbox renewal, domain renewal, DID renewal, CCTS renewal, T2 tax return, corporate tax payment, GST/HST return, T4/T4A slips, BC PST, WorkSafeBC, CRTC registration update, plus ATS survey forms (REP-T/T1 mandatory for all carriers)
- Billable items generate ERPNext Sales Invoice; entry cannot be completed until paid
- On payment: entry marked Completed, admin ToDo created, next-year entry auto-created
- Webhook-triggered:
handle_renewal_paymentin job_server for immediate processing
DocServer (Windows VM)
MinIO-based transport — no direct HTTP connection between Linux and Windows VMs.
Workers: upload DOCX to minio://performancewest/to-convert/{uuid}.docx
→ DocServer polls to-convert/ bucket every 12 seconds
→ Word COM converts DOCX → PDF
→ DocServer uploads PDF to minio://performancewest/converted/{uuid}.pdf
→ Workers poll converted/ bucket for result
- Heartbeat: DocServer writes
docserver-heartbeat.jsonto MinIO every 60 seconds - Fallback: If heartbeat is stale (>5 min), workers auto-switch to LibreOffice headless
Boot Sequence
All services auto-start on reboot via systemd:
| Unit | What it starts | Depends on |
|---|---|---|
docker.service |
Docker daemon | network |
k3s.service |
k3s + all SHKeeper pods | docker |
nginx.service |
Reverse proxy + TLS + RPC proxies (:5555/:5556) | network |
performancewest.service |
Prod docker compose (15 containers) | docker |
performancewest-dev.service |
Dev docker compose (4 containers) | docker, performancewest |
All containers use restart: unless-stopped. k3s manages pod restarts via deployment specs. The nginx RPC proxy configs for TronGrid (:5555) and ETH (:5556) are in /etc/nginx/conf.d/ and load automatically. UFW rules for ports 5555/5556 are persistent (allow from 10.42.0.0/16 only).
Scheduled Workers (systemd timers)
Deployed via infra/ansible/roles/worker-crons/. Each timer runs docker compose exec -T workers python -m <module>.
| Timer | Cadence | Module |
|---|---|---|
pw-usf-factor-monitor |
daily 09:00 CT | scripts.workers.usf_factor_monitor |
pw-deminimis-factor-check |
daily 03:00 UTC | scripts.workers.deminimis_factor_check |
pw-cold-wallet-sweep |
every 30 min | scripts.workers.cold_wallet_sweeper |
pw-crypto-payment-worker |
every 60s | scripts.workers.crypto_payment_worker |
pw-relay-deposit-monitor |
every 5 min | scripts.workers.relay_deposit_monitor |
pw-commission-worker |
daily 02:00 UTC | scripts.workers.commission_worker |
pw-renewal-worker |
daily 04:00 UTC | scripts.workers.renewal_worker |
pw-cdr-retention |
daily 05:00 UTC | scripts.workers.cdr_retention_sweeper |
pw-cdr-unlock-nudge |
daily 10:00 CT | scripts.workers.cdr_unlock_nudge |
pw-payment-reminder |
daily 11:00 CT | scripts.workers.payment_reminder |
pw-fcc-rmd-removed |
weekly Wed 08:00 CT | scripts.workers.fcc_rmd_removed_scraper |
pw-client-email-processor |
every 15 min | scripts.workers.client_email_processor |
pw-amb-location-scraper |
daily 06:00 UTC | scripts.workers.amb_location_scraper |
Database Schema (67 migrations)
Key tables added in this cycle:
| Table | Migration | Purpose |
|---|---|---|
jurisdictions |
066 | Unified US states + CA provinces (55 rows) |
foreign_qualification_registrations |
066 | Per-state COA filings |
state_compliance_obligations |
067 | Annual report fees/dates for all 51 US jurisdictions |
crypto_payment_ledger |
062 | Immutable append-only money movement ledger |
crypto_payment_jobs |
065 | Treasury state machine (received → settled) |
vendor_obligations |
063 | Sizer: filing fees + commission reserves |
cold_wallet_config / cold_wallet_sweeps |
064 | Cold wallet management |
FCC Compliance Check Tool
Public at /tools/fcc-compliance-check. Queries FRN against CORES, RMD, 499 Filer DB, and CPNI records. Displays:
- CORES registration status (red-light check)
- RMD filing + certification date
- STIR/SHAKEN implementation (self-reported from RMD, hidden until STI-PA API access)
- CPNI annual certification (past-due detection with correct deadline logic)
- Form 499-A annual filing (CY-year-aware)
- Form 499-Q quarterly (de minimis trade-off explanation)
- BDC broadband/voice interactive toggle (two-step questionnaire)
Remediation CTA links to /order/fcc-compliance?services=cpni,499a,... for bundled ordering with 15% discount.
Compliance Service Catalog (34 handlers)
| Slug | Handler | Price |
|---|---|---|
new-carrier-bundle |
NewCarrierBundleHandler (6 sub-handlers) | $1,799 |
fcc-full-compliance |
FullComplianceHandler | $1,499 |
fcc-499a |
Form499AHandler | $499 |
fcc-499a-499q |
Form499ABundleHandler | $599 |
cpni-certification |
CPNIFilingHandler (9 category variants) | $149 |
rmd-filing |
RMDFilingHandler | $219 |
calea-ssi |
CALEASSIHandler (6 category variants) | $299 |
bdc-filing / bdc-broadband / bdc-voice |
BDCFilingHandler | $299/$199/$149 |
foreign-qualification-single |
ForeignQualificationHandler | $149 + state fees |
foreign-qualification-multi |
ForeignQualificationHandler | $99/state + fees |
dc-agent |
DCAgentHandler (NWRA wholesale) | $99/yr |
cores-frn-registration |
CORESFRNRegistrationHandler | $99 |
fcc-499-initial |
Form499InitialHandler | $299 |
ocn-registration |
OCNRegistrationHandler | $799 |
State Formation Adapters
10 states with real adapters (name search via Playwright + portal config): WY (237 lines), CO (177), DE (119), FL (118), TX (SOSDirect), NV (SilverFlume), UT (DCC), NM, OH, MT. ~40 remaining states have stub adapters that create admin todos for manual filing.
JurisdictionConfig abstraction at scripts/formation/jurisdictions/ reads from the jurisdictions DB table (migration 066) and provides per-state fees, portal URLs, NWRA addresses, and entity type catalogs.
Deployment
Production: ansible-playbook infra/ansible/playbooks/deploy.yml -i infra/ansible/inventory/hosts.yml
Dev: bash scripts/deploy-dev.sh — rsyncs source files + rebuilds Docker containers. Static pages (tools, services, homepage) are in site/public/ and survive Astro rebuilds.
nginx cache policy: _astro/ = immutable (1 year). HTML = no-cache. Images = 30 days.
- Atomic uploads:
.tmp_prefix +copy_objectrename prevents partial reads