Sneaker care scheduling, provider dispatch, notifications, and payment workflow demo.
This repository is a small monorepo. Run each app from its own folder:
apps/api- FastAPI backend, Postgres schema, demo seed, workersapps/mobile- Expo React Native appapps/payment- optional Stripe-backed payment service
- New developer onboarding
- Central environment variable reference
- Architecture overview
- Mobile architecture
- API architecture
- Payment architecture
- Deployment architecture
- Staging runbook
- Troubleshooting
- OpenSpec workflow
The local workflow below is Windows PowerShell first because that is the current development environment. macOS/Linux equivalents are mostly the same, except virtual environment activation paths and shell environment-variable syntax.
- Python 3.11+
- Node.js and npm
- Docker Desktop
- Expo Go for device testing, or an Android/iOS simulator
- Optional: Stripe CLI and Stripe test credentials for real payment-service testing
- Optional: Google Directions API key for route polylines and ETA cards
The simplest local startup is:
.\scripts\start-local.ps1That command:
- starts Postgres with Docker Compose
- creates
apps/api/.venvif needed - creates and normalizes
apps/api/.envfor Windows host-to-Docker development - installs API dependencies unless
-SkipInstallis passed - runs Alembic migrations
- starts the API in a separate PowerShell window
- seeds demo data
- starts Expo in the current window
Common variants:
# Use the Mt. Juliet demo seed
.\scripts\start-local.ps1 -DemoMarket mt_juliet
# Use Expo tunnel for Metro connectivity
.\scripts\start-local.ps1 -Tunnel
# Skip dependency installation on repeat startups
.\scripts\start-local.ps1 -SkipInstall
# Use another API port if 8000 is already occupied
.\scripts\start-local.ps1 -Port 8002 -ApiBaseUrl "http://<YOUR-LAN-IP>:8002"
# Physical device on the same LAN
.\scripts\start-local.ps1 -ApiBaseUrl "http://<YOUR-LAN-IP>:8000"
# Physical device with Expo tunnel, matching npm start -- --tunnel
.\scripts\start-local.ps1 -ApiBaseUrl "http://192.168.1.14:8000" -Tunnel
# Service payment mode with local payment service and Expo Go return URL
.\scripts\start-local.ps1 `
-ApiBaseUrl "http://192.168.1.14:8000" `
-DemoMarket mt_juliet `
-Tunnel `
-PaymentMode service `
-MobileRedirectBase "exp://192.168.1.14:8081/--"
# Same service-mode startup, but with API on port 8002
.\scripts\start-local.ps1 `
-Port 8002 `
-ApiBaseUrl "http://192.168.1.14:8002" `
-DemoMarket mt_juliet `
-Tunnel `
-PaymentMode service `
-MobileRedirectBase "exp://192.168.1.14:8081/--"You can also start each side independently:
.\scripts\start-api.ps1
.\scripts\start-mobile.ps1For an Android emulator, point mobile at the emulator host bridge:
.\scripts\start-mobile.ps1 -ApiBaseUrl "http://10.0.2.2:8000"Manual setup commands are below for troubleshooting and non-Windows shells.
scripts/start-api.ps1 prepares and runs the backend stack:
.\scripts\start-api.ps1Useful options:
-DemoMarket shelbyor-DemoMarket mt_juliet-NoSeedto skip thePOST /dev/seedcall-ResetDbto rundocker compose down -vbefore starting Postgres-SkipInstallto skippip install-PaymentMode mockor-PaymentMode service-PaymentServiceBaseUrl "http://localhost:8001"for service payment mode-MobileRedirectBase "exp://<YOUR-LAN-IP>:8081/--"for Expo Go returns, orshoeinn://appfor a dev build-Port 8000to override the API port
If apps/api/.env already exists, the scripts preserve existing payment settings unless you explicitly pass -PaymentMode, -PaymentServiceBaseUrl, or -MobileRedirectBase.
scripts/start-mobile.ps1 prepares Expo environment variables and starts the mobile app:
.\scripts\start-mobile.ps1 -ApiBaseUrl "http://localhost:8000"Useful options:
-ApiBaseUrl "http://10.0.2.2:8000"for Android emulator-ApiBaseUrl "http://<YOUR-LAN-IP>:8000"for a physical device-DemoMarket shelbyor-DemoMarket mt_julietto choose which demo login buttons Expo displays-ExpectedPaymentMode serviceto fail fast if the API is not actually running in service payment mode-Tunnelto runnpm start -- --tunnel-SkipApiCheckto skip the preflightGET /healthcheck-SkipInstallto skipnpm install
scripts/start-payment.ps1 prepares and runs the optional Stripe payment service:
.\scripts\start-payment.ps1Useful options:
-Port 8001to override the payment service port-SkipInstallto skippip install -e .
The payment script expects apps/payment/.env to contain STRIPE_API_KEY and STRIPE_WEBHOOK_SECRET.
scripts/start-local.ps1 opens the API script in a separate PowerShell window, waits briefly, then starts Expo in the current window.
When -PaymentMode service is passed, start-local.ps1 also starts apps/payment in a separate PowerShell window before starting the API. The local payment service defaults to http://localhost:8001.
Useful options:
-Port 8002to pass a non-default API port through tostart-api.ps1-ApiBaseUrl "http://<YOUR-LAN-IP>:8002"to point Expo at that same API port on a physical device-PaymentPort 8001to override the local payment service port-PaymentServiceBaseUrl "http://localhost:8001"to point the API at a specific payment service URL-SkipPaymentServiceto use an already-running payment service without auto-starting one
apps/api/docker-compose.yml runs Postgres only:
cd .\apps\api
docker compose up -dIf your Docker install only supports the old command, use docker-compose up -d.
Create and install the backend environment:
cd .\apps\api
py -3.11 -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r ..\..\requirements.txt
pip install -e .Create .env:
Copy-Item .env.example .envImportant local database setting:
- Use
localhostinDATABASE_URLwhen the API runs directly on Windows and Postgres runs in Docker. - Use
dbonly from another container on the same Docker network.
Known-good Windows host value:
DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/shoeinnIf you previously started Postgres with different credentials, the Docker volume keeps them. To reset the local DB and delete all local data:
docker compose down -v
docker compose up -d
python -m alembic upgrade headStart the API:
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000Health checks:
Invoke-RestMethod http://localhost:8000/health
Invoke-RestMethod http://localhost:8000/ready/health only confirms the process is alive. /ready also checks the database, migration heads, and required notification table.
Seed the default Shelby County, Alabama demo:
Invoke-RestMethod -Method Post "http://localhost:8000/dev/seed?reset=true"Seed the Mt. Juliet, Tennessee demo:
Invoke-RestMethod -Method Post "http://localhost:8000/dev/seed?reset=true&demo_market=mt_juliet"Default demo logins all use Password1!:
- Global admin:
admin@shoeinn.com - Customer:
customer@shoeinn.com - Pelham owner:
pelham.admin@shoeinn.com - Pelham providers:
pelham.driver1@shoeinn.com,pelham.driver2@shoeinn.com - Helena owner:
helena.admin@shoeinn.com - Helena provider:
helena.driver@shoeinn.com - Alabaster owner:
alabaster.admin@shoeinn.com - Alabaster provider:
alabaster.driver@shoeinn.com
Mt. Juliet quick-demo logins use Password123!:
- Customer:
customer.mtjuliet@shoeinn.demo - Provider:
provider.mtjuliet@shoeinn.demo - Company admin:
admin.mtjuliet@shoeinn.demo
The seed response also returns the current login list and generated company IDs.
When reset=true is used, the seed endpoint clears all known demo markets before creating the requested market. That keeps Shelby/Helena records from showing up after reseeding Mt. Juliet.
Install dependencies:
cd .\apps\mobile
npm installSet the API base URL before starting Expo. Set both names for now: most code reads EXPO_PUBLIC_API_BASE_URL, while a service-admin helper still reads EXPO_PUBLIC_API_URL.
Windows host or iOS simulator:
$env:EXPO_PUBLIC_API_BASE_URL="http://localhost:8000"
$env:EXPO_PUBLIC_API_URL=$env:EXPO_PUBLIC_API_BASE_URL
npm startAndroid emulator:
$env:EXPO_PUBLIC_API_BASE_URL="http://10.0.2.2:8000"
$env:EXPO_PUBLIC_API_URL=$env:EXPO_PUBLIC_API_BASE_URL
npm startPhysical device on the same LAN:
$env:EXPO_PUBLIC_API_BASE_URL="http://<YOUR-LAN-IP>:8000"
$env:EXPO_PUBLIC_API_URL=$env:EXPO_PUBLIC_API_BASE_URL
npm startUse Expo tunnel only for Metro connectivity if needed:
npx expo start --tunnelTunnel mode does not automatically expose the backend API. A phone still needs an API URL it can reach.
Optional mobile environment values:
$env:EXPO_PUBLIC_GOOGLE_MAPS_API_KEY="your-google-directions-api-key"
$env:EXPO_PUBLIC_MOBILE_REDIRECT_BASE="exp://<YOUR-LAN-IP>:8081/--"Use EXPO_PUBLIC_MOBILE_REDIRECT_BASE="shoeinn://app" for a dev build or standalone app that supports the custom scheme.
Most local development should keep the API in mock payment mode:
PAYMENT_MODE=mock
PAYMENT_SERVICE_BASE_URL=
PAYMENT_MOBILE_REDIRECT_BASE=Only start apps/payment when validating the Stripe-hosted checkout path.
Start the payment service:
cd .\apps\payment
py -3.11 -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -e .
$env:STRIPE_API_KEY="sk_test_..."
$env:STRIPE_WEBHOOK_SECRET="whsec_..."
$env:DATABASE_URL="sqlite:///./payment.db"
$env:TENANT_ID="public"
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8001Then set these API values in apps/api/.env:
PAYMENT_MODE=service
PAYMENT_SERVICE_BASE_URL=http://localhost:8001
PAYMENT_MOBILE_REDIRECT_BASE=shoeinn://appFor Expo Go return-flow testing, use an explicit Expo URL instead:
PAYMENT_MOBILE_REDIRECT_BASE=exp://<YOUR-LAN-IP>:8081/--Forward Stripe webhooks in a separate shell when testing automatic reconciliation:
stripe listen --forward-to http://localhost:8001/payments/webhooks/stripeIf you want the payment service to push status back to the booking API:
$env:BOOKING_API_WEBHOOK_URL="http://localhost:8000/webhooks/payments"Use this path when validating real Stripe Checkout from mobile:
- Start
apps/paymentwith Stripe test keys. - Start
apps/apiwithPAYMENT_MODE=service,PAYMENT_SERVICE_BASE_URL, andPAYMENT_MOBILE_REDIRECT_BASE. - Start mobile with the correct LAN API URL.
- Select
Add new card in secure checkout. - Tap
Place Bookingand verify Stripe Checkout opens immediately. - Cancel Checkout and verify the appointment remains visible as payment pending.
- Reopen the appointment and use
Open secure checkout,Check payment status, orCancel unpaid booking. - Complete payment and verify return or
Check payment statusmoves the appointment to paid/confirmed.
The API process starts the payment sync worker only when:
PAYMENT_MODE=servicePAYMENT_SERVICE_BASE_URLis configuredENABLE_PAYMENT_SYNC_WORKERis not disabled
The notification worker is manual:
cd .\apps\api
.\.venv\Scripts\Activate.ps1
python -m app.workers.notification_workerStart it only when you need queued email, push, or in-app delivery validation.
Run backend tests:
cd .\apps\api
.\.venv\Scripts\Activate.ps1
python -m pytest tests -qRun a focused backend test file:
python -m pytest tests\test_assignment_claiming.py -qThe backend tests use in-memory SQLite through apps/api/tests/conftest.py, so they do not require local Postgres, migrations, or seeded data.
Run mobile tests and type checks:
cd .\apps\mobile
npm test -- --runInBand
npm run typecheckRun payment service tests:
cd .\apps\payment
.\.venv\Scripts\Activate.ps1
python -m pytest tests -qRecommended pre-demo validation:
cd .\apps\api
python -m pytest tests\test_assignment_claiming.py tests\test_dev_seed.py tests\test_payment_gateway.py -q
cd ..\mobile
npm test -- --runInBand
npm run typecheckpassword authentication failed for user
The Postgres Docker volume likely has credentials from an older run. Reset it from apps/api:
docker compose down -v
docker compose up -d
python -m alembic upgrade headMobile cannot reach the API
- Android emulator should use
http://10.0.2.2:8000. - Physical devices should use
http://<YOUR-LAN-IP>:8000. - Confirm the API is bound to
0.0.0.0. - Confirm Windows Firewall allows inbound traffic to port
8000when using a physical device. - If using
-Port 8002, update both values:-Port 8002 -ApiBaseUrl "http://<YOUR-LAN-IP>:8002". - If
http://localhost:<PORT>/healthworks buthttp://<YOUR-LAN-IP>:<PORT>/healthdoes not work from the same machine, the issue is the LAN IP or Windows Firewall rule for that port.
Payment confirmation fails in service mode
- Confirm
PAYMENT_SERVICE_BASE_URLis set. - Confirm
PAYMENT_MOBILE_REDIRECT_BASEis not blank or placeholder. - Keep
PAYMENT_MODE=mockif the payment service is not intentionally running.
Push warning: No projectId found
- Run
eas initinapps/mobile. - Ensure
extra.eas.projectIdis configured in the Expo app config.
Notifications are not sending
- Confirm the notification worker is running.
- Check
notification_outbox.status. - Verify push tokens exist for the target users.
GET /health- process healthGET /ready- database and migration readinessPOST /dev/seed?reset=true- reset and seed default demo dataPOST /dev/seed?reset=true&demo_market=mt_juliet- reset and seed Mt. Juliet demo dataGET /companies- list companiesGET /services- list servicesPOST /auth/login- login
Example login:
Invoke-RestMethod -Method Post "http://localhost:8000/auth/login" `
-ContentType "application/json" `
-Body '{"email":"customer@shoeinn.com","password":"Password1!"}'