Drop-in white-label SaaS starter built on MIOSA. Fork this repo to give your existing users sandboxed compute environments under your own brand — no MIOSA UI exposed, no infrastructure to manage. Shipping time: one afternoon.
sequenceDiagram
actor User as End-user browser
participant FE as Your React frontend
participant BE as Your FastAPI backend
participant MIOSA as MIOSA API
User->>FE: Visits your app (your domain, your brand)
FE->>BE: POST /api/auth/login (your JWT auth)
BE-->>FE: { access_token: "eyJ..." }
User->>FE: Clicks "Open sandbox"
FE->>BE: POST /api/sandboxes/{id}/preview-token
note over BE,MIOSA: msk_* key stays server-side
BE->>MIOSA: sandboxes.preview_token(id, expires_in=3600)
MIOSA-->>BE: { value: "mp_...", expires_at }
BE-->>FE: { token: "mp_...", expires_at }
FE->>FE: <MiosaPreview token="mp_..." />
note over FE,MIOSA: @miosa/react opens WebSocket using mp_* token
FE-->>User: Live sandbox UI — your brand, your layout
The three rules this architecture enforces:
- The
msk_*API key lives only in your FastAPI process. Never in the browser. - Preview tokens (
mp_*) are short-lived (1 h) and scoped to a single sandbox. - Every MIOSA resource is tagged with
external_user_id— your user, your data.
# 1. Clone and enter
git clone https://github.com/Miosa-osa/whitelabel-react-python
cd whitelabel-react-python
# 2. Copy env and fill in the 3 required values
cp .env.example .env
# Edit .env:
# MIOSA_API_KEY → from miosa.app → Settings → API Keys
# JWT_SECRET → python -c "import secrets; print(secrets.token_urlsafe(32))"
# (MIOSA_WEBHOOK_SECRET is optional for local dev)
# 3. Start the stack
docker compose up
# 4. Open the app
open http://localhost:5173FastAPI Swagger UI is at http://localhost:8000/docs — useful for exploring the API.
| What | File | Notes |
|---|---|---|
| Brand colors | frontend/src/styles.css → :root CSS vars |
Change --color-primary and friends |
| App name + logo | frontend/public/logo.svg, frontend/index.html <title> |
Drop in your SVG |
| Auth provider | backend/app/routes/auth.py |
Swap for Auth0/Clerk/Supabase |
| Sandbox images | backend/app/routes/sandboxes.py → CreateSandboxRequest.image |
Expose your templates |
| Layout | frontend/src/pages/Studio.tsx |
Rearrange the three panel components |
| Admin controls | frontend/src/pages/Admin.tsx |
Show/hide tabs your users need |
| Database | .env → DATABASE_URL |
SQLite by default; swap for postgresql+asyncpg:// |
See docs/CUSTOMIZE.md for before/after code for each change.
msk_w_live_* Master key — your server only, never browser
└─ mp_preview_* Short-lived (1 h), single sandbox — goes to browser
└─ share_* Read-only share links — optional, time-limited
- msk_* can create/delete sandboxes and list all tenant resources. Leaking it would compromise your entire tenant. It lives in your FastAPI process only.
- mp_* tokens are minted on demand, scoped to one sandbox, and expire in 1 hour. Even if intercepted, blast radius is one sandbox for one hour.
- Your JWT is separate from both MIOSA token types. MIOSA never sees your user credentials.
MIOSA signs every webhook payload with HMAC-SHA256. The backend verifies the signature before trusting the event:
# backend/app/routes/webhooks.py
expected = hmac.new(settings.MIOSA_WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(f"sha256={expected}", x_miosa_signature):
raise HTTPException(401, "bad signature")Render / Railway / Fly.io — simplest path:
# Render: set env vars in dashboard, point to ./backend, start command:
uvicorn app.main:app --host 0.0.0.0 --port $PORT
# Fly.io:
fly launch --dockerfile backend/Dockerfile
fly secrets set MIOSA_API_KEY=msk_w_live_... JWT_SECRET=...Before going to production:
- Replace SQLite with PostgreSQL:
DATABASE_URL=postgresql+asyncpg://user:pass@host/db - Add Alembic migrations — remove
create_all()fromlifespan() - Add rate limiting (e.g.,
slowapi) to/api/auth/*endpoints - Enable HTTPS and set
FRONTEND_ORIGINto your production domain
Vercel — recommended:
cd frontend
vercel deploy
# Set VITE_API_BASE=https://your-api.onrender.comOr build and serve static files:
cd frontend && npm run build
# Copy dist/ to any static host (Cloudflare Pages, S3, nginx)In production, configure your reverse proxy (nginx/Caddy) to forward /api/* to your
FastAPI backend instead of relying on Vite's dev proxy.
See docs/CUSTOMIZE.md for detailed before/after code for:
- Swapping auth to Auth0, Supabase, or Clerk
- Adding a custom sandbox image / template
- Migrating from Vite to Next.js App Router
- Replacing FastAPI with Flask or Django
- Adding managed databases or deployments from MIOSA
- Multi-tenant subdomain routing
.
├── .env.example # 5 vars — fill in 3, you're running
├── docker-compose.yml
├── backend/
│ ├── Dockerfile
│ ├── pyproject.toml
│ └── app/
│ ├── main.py # FastAPI app + lifespan
│ ├── settings.py # Pydantic settings from env
│ ├── deps.py # current_user + miosa_client DI
│ ├── models.py # ORM + Pydantic schemas
│ ├── database.py # SQLAlchemy async engine
│ └── routes/
│ ├── auth.py # /auth/login /auth/register /auth/me
│ ├── sandboxes.py # CRUD scoped to current user
│ ├── tokens.py # POST preview-token (ownership verified)
│ ├── shares.py # POST share link
│ ├── usage.py # GET /me/usage
│ ├── webhooks.py # POST /webhooks/miosa (HMAC verified)
│ └── admin.py # /admin/* tenant controls
├── frontend/
│ ├── Dockerfile
│ ├── nginx.conf
│ ├── vite.config.ts
│ ├── tsconfig.json
│ └── src/
│ ├── main.tsx
│ ├── App.tsx # Router
│ ├── api.ts # Typed client → YOUR backend only
│ ├── auth.tsx # AuthContext (your JWT, not MIOSA)
│ ├── styles.css # One file, easy to swap
│ └── pages/
│ ├── Login.tsx
│ ├── Login.test.tsx # Component tests (vitest + RTL)
│ ├── Dashboard.tsx # Sandbox list
│ ├── Studio.tsx # MiosaPreview + MiosaTerminal + MiosaFileTree
│ └── Admin.tsx # Tenant admin panel
└── docs/
└── CUSTOMIZE.md
# Backend
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
cp ../.env.example .env # fill in values
uvicorn app.main:app --reload
# Frontend (separate terminal)
cd frontend
npm install
npm run dev
# Vite proxies /api → http://localhost:8000
# Tests
cd backend && pytest tests/ -v
cd frontend && npm test