Vela Platform v2
Single FastAPI application that powers the Vela business — a home server installation service in Casablanca, Morocco.
Built: June 26, 2026
Deployed: Proxmox CT 124 @ 192.168.1.244
Architecture
┌─────────────────────────────────────────┐
│ nginx :80 │
│ ├── / → Marketing site │
│ ├── /admin → Admin P&L dashboard │
│ ├── /backup → Backup explainer │
│ ├── /static/* → Logos, assets │
│ └── /api/* → JSON API │
│ │ │
│ uvicorn :8000 (systemd: vela.service) │
│ ├── main.py — FastAPI app │
│ ├── models.py — SQLAlchemy models │
│ ├── database.py — SQLite WAL mode │
│ └── auth.py — JWT auth │
│ │
│ vela.db (SQLite) │
└─────────────────────────────────────────┘
One box. One app. One database. No microservices.
What It Does
Public Marketing Site (/)
- Bilingual FR/EN with URL-based language toggle (
?lang=fr/?lang=en) - Dark/light mode toggle (persisted in localStorage)
- Hero section with value proposition
- Problem/solution section
- Live pricing cards pulled from API
- Immich & Nextcloud sections with real SVG logos
- "How it works" — 3-step visual
- WhatsApp CTA buttons (all pointing to +212 725-569519)
- Footer with barely-visible "Admin" link (30% opacity)
Backup Explainer (/backup)
- Explains backups to non-technical users
- Simple vs redundant backup comparison
- "The golden rule: if it only exists in one place, it doesn't really exist"
- Bilingual
Admin Panel (/admin)
- JWT login (username:
oimwiodev, password:kxdr781020) - Token stored in localStorage, verified via
/api/verify - 4 tabs: Dashboard, Tiers, Transactions, Expenses
Dashboard
- Real-time P&L cards: Revenue, COGS, Gross Profit, Margin %, Expenses, Net Profit
- Monthly breakdown table with month filter
- Formula: Revenue − COGS − Expenses = Net Profit (not the fake v1 math)
Tiers
- 4 tiers: Rif, Rif+, Atlas, Atlas+
- Component breakdown with unit costs
- Inline edit: tier name, sell price, component costs
- Toggle active/inactive
- Cost price auto-calculated from components
- Margin % shown per tier
Transactions
- Add manual sale (select tier → snapshots sell price + cost at time of sale)
- Filter by month
- Delete with confirm + undo timer:
- 1st click: "Sure?" (4s timeout)
- 2nd click: deletes, row fades, 8s undo window
Expenses
- Add/delete expenses with month and category
- Monthly expense templates (auto-generate from templates)
- Soft delete (deleted_at timestamp)
API Endpoints
Public (no auth)
| Method | Path | Description |
|---|---|---|
| GET | /api/public/pricing |
Active tiers: name, description, sell_price |
| GET | /api/public/services/{id} |
Single tier details + components |
Auth
| Method | Path | Description |
|---|---|---|
| POST | /api/auth/login |
Login (username + password) → JWT |
| POST | /api/auth/logout |
Clear cookie |
| GET | /api/verify |
Verify JWT, returns email |
Admin (JWT required)
| Method | Path | Description |
|---|---|---|
| GET | /api/pnl |
P&L summary + monthly breakdown |
| GET | /api/services |
All tiers with components |
| PUT | /api/services/{id} |
Update tier (name, price, active, etc.) |
| PUT | /api/services/{id}/toggle |
Toggle active/inactive |
| PUT | /api/services/{id}/components/{cid} |
Update component cost/qty |
| GET | /api/transactions |
List transactions (optional ?month=) |
| POST | /api/transactions |
Create sale (snapshots prices) |
| DELETE | /api/transactions/{id} |
Delete transaction |
| GET | /api/expenses |
List expenses |
| POST | /api/expenses |
Add expense |
| DELETE | /api/expenses/{id} |
Soft-delete expense |
| GET | /api/expense-templates |
List recurring templates |
| POST | /api/expenses/generate-month |
Generate from templates |
Database Schema
6 tables in SQLite (vela.db):
| Table | Purpose |
|---|---|
users |
Single admin (email, bcrypt password) |
services |
4 tiers (name, sell_price, cost_price, active, sort_order) |
service_components |
Per-tier parts (name, unit_cost, quantity) |
transactions |
Frozen sales with price/cost snapshots, unique (source_type, source_id) |
expenses |
Monthly costs with soft delete |
expense_templates |
Recurring expense patterns |
Key design: transactions freeze sell_price and cost_price at time of sale. If you change an SSD cost later, old sales don't change. Real P&L math.
SQLite pragmas: journal_mode=WAL, foreign_keys=ON, busy_timeout=5000
Deployment
Server: Proxmox CT 124 (Debian 12)
IP: 192.168.1.244
Service: systemctl restart vela
Files: /opt/vela/
DB: /opt/vela/vela.db
Update workflow
# 1. Edit files locally
# 2. Package and deploy
cd ~/src/vela-platform
tar czf /tmp/vela-update.tar.gz main.py templates/
sshpass -p 'PW' scp /tmp/vela-update.tar.gz root@192.168.1.109:/tmp/
sshpass -p 'PW' ssh root@192.168.1.109 "
pct push 124 /tmp/vela-update.tar.gz /tmp/vela-update.tar.gz &&
pct exec 124 -- bash -c 'cd /opt/vela && tar xzf /tmp/vela-update.tar.gz && systemctl restart vela'
"
Public access
Point Nginx Proxy Manager at 192.168.1.244:80 for your domain.
Design Decisions
- No Jinja2 — hit a cache bug (
unhashable type: 'dict'). Switched toFileResponseserving raw HTML. All dynamic content viafetch()to JSON APIs. - No framework — vanilla HTML/CSS/JS. No React, no Vue, no build step.
- CSS variables — dark/light mode is a single
data-themeattribute. Zero hardcoded hex colors in JS. - Single admin — no user management. One account. Simple.
- Public API is read-only — pricing only. No costs, no margins, no internals leak.
WhatsApp Sales Bot
A separate Hermes agent with a custom SOUL.md handles WhatsApp conversations. The bot:
- Answers questions in French/Darija/English
- Fetches live pricing via
curl http://192.168.1.244/api/public/pricing - Follows a 4-step sales process: understand → recommend → handle objections → close
- Never hardcodes prices — always checks the API
- Personality: calm, direct, short messages, no corporate tone
SOUL.md at /tmp/vela-sales-bot-soul.md
What Was Removed (v1)
The old Vela had two separate LXCs (CT125 + CT126) with three Python services (two FastAPI apps + nginx, two auth systems, two databases). All nuked. Replaced with this single monolith.