Minimal proof-of-concept for PaceCtrl with a FastAPI backend and an embeddable widget bundle. No database is used; intents are stored in memory for the lifetime of the process.
Location: backend/
Requirements: Python 3.11+
Install and run locally:
cd backend
python -m venv .venv
.\.venv\Scripts\activate
pip install -r requirements.txt
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reloadKey endpoints:
GET /health– service checkGET /api/v1/public/widget/config?external_trip_id=HEL-TLL-2025-12-12– returns static trip config + theme (multiple trips supported; seeTRIP_CONFIGSinbackend/app/main.py)POST /api/v1/public/choice-intentswith{ external_trip_id, reduction_pct }– validates bounds, stores intent in memory, returns{ intent_id }GET /api/v1/admin/choice-intents– lists all intents stored in memory (testing only, cleared on restart)POST /api/v1/public/choice-confirmationswith{ booking_id, intent_id }– moves an intent to the confirmed listGET /api/v1/admin/choice-confirmations– lists confirmed choices (testing only)GET /api/v1/admin/trip-average?external_trip_id=...– returns count and average reduction_pct for confirmed choices of the tripGET /api/v1/admin/stream– Server-Sent Events (SSE) stream with live intents, confirmations, and trip averagesGET /widget.js– serves the built widget bundle fromwidget/dist/widget.js
CORS is fully open to allow embedding from external origins.
Location: widget/
Build the UMD-style bundle (exposed as window.PaceCtrlWidget):
cd widget
npm install
npm run buildOutput: widget/dist/widget.js (served by the backend).
Init API:
<div id="pacectrl-widget" data-external-trip-id="HEL-TLL-2025-12-12"></div>
<script src="https://YOUR-RAILWAY-URL/widget.js"></script>
<script>
window.PaceCtrlWidget.init({
container: "#pacectrl-widget",
apiBaseUrl: "https://YOUR-RAILWAY-URL",
// optional
onIntentCreated: (intent) => console.log(intent)
});
</script>React/local test snippet (index.html inside your app):
<div id="pacectrl-widget" data-external-trip-id="HEL-TLL-2025-12-12"></div>
<script src="https://YOUR-RAILWAY-URL/widget.js"></script>
<script>
window.PaceCtrlWidget.init({
container: "#pacectrl-widget",
apiBaseUrl: "https://YOUR-RAILWAY-URL"
});
</script>A simple manual demo is available at widget/demo.html; it loads the local bundle and posts to http://localhost:8000 by default. Adjust apiBaseUrl in the script if using Railway.
- Ensure
widget/dist/widget.jsexists (npm run buildinwidget/). Commit artifacts if deploying directly from repo. - Create a new Railway service from this folder, set the root to
backend/. - Railway will read
backend/Procfileand start:uvicorn app.main:app --host 0.0.0.0 --port $PORT. - No env vars are required for this milestone.
- After deploy, your public widget URL is
https://<railway-host>/widget.jsand API base ishttps://<railway-host>.
- Static trip is hardcoded to
HEL-TLL-2025-12-12with the provided speed/theme values. - Intents live in memory only (cleared on restart) until Postgres is added later.
- CORS allows all origins to simplify embedding during MVP.