Contract-first full-stack gym platform built on an existing FastAPI backend with a pnpm monorepo for the React web app, Expo mobile app, and a shared generated API client.
This project now combines the existing backend with a scalable frontend ecosystem:
- member management
- authentication and role-based access
- class scheduling
- booking and waitlist handling
- attendance tracking
- Strava account connection and activity sync
- Redis-backed caching
- Celery workers for asynchronous jobs
- React web dashboard powered by TanStack Query and React Router
- Expo mobile app powered by React Navigation and TanStack Query
- shared OpenAPI-generated TypeScript types and
openapi-fetchclient runtime
The backend keeps its clean service/repository split, while the frontend apps share one generated contract so API types are not duplicated across platforms.
- Python 3.11+
- FastAPI
- PostgreSQL
- SQLAlchemy 2 async ORM
- Alembic
- Redis
- Celery
- Docker Compose
- Pydantic v2
- JWT bearer auth
- pnpm workspaces
- React + Vite + TypeScript
- Expo + React Native + TypeScript
- TanStack Query
- React Router
- React Navigation
- Tailwind CSS
- openapi-typescript
- openapi-fetch
apps/
mobile/
web/
packages/
api-client/
config/
app/
api/
routes/
core/
domain/
models/
infrastructure/
cache/
database/
integrations/
repositories/
schemas/
services/
workers/
migrations/
docker/
docker-compose.yml
package.json
pnpm-workspace.yaml
requirements.txt
README.md
The FastAPI backend intentionally stays at the repository root so Railway, Docker, and existing Python paths continue to work without backend logic changes.
app/api: FastAPI route layer and request dependenciesapp/core: settings, security, logging, exceptionsapp/domain: enums and SQLAlchemy modelsapp/repositories: persistence access logicapp/services: business rules and orchestrationapp/infrastructure: DB session, Redis, external clientsapp/workers: Celery app and background tasksmigrations: Alembic environment and schema revisionsapps/web: React + Vite dashboard using the shared API packageapps/mobile: Expo app using the same shared API hooks and auth modelpackages/api-client: generated OpenAPI schema, typed client, shared auth/session utilities, and shared React Query hookspackages/config: shared TypeScript, ESLint, and Prettier baselines
pnpm installpnpm generate:apiThis runs:
openapi-typescript https://gymappback-production-7f4e.up.railway.app/openapi.json -o packages/api-client/schema.tspnpm dev:web
pnpm dev:mobilepnpm typecheck
pnpm lint
pnpm build:web
pnpm build:mobilebuild:mobile exports native bundles for iOS and Android into apps/mobile/dist/.
The shared contract-first layer lives in packages/api-client:
schema.ts: generated OpenAPI typesclient.ts:openapi-fetchclient factory with auth-aware request handlingauth.ts: platform-agnostic session storage and refresh-ready auth utilitieshooks.ts: shared TanStack Query hooks such asuseWorkouts,useWorkout,useLogin, anduseCreateBooking
The web and mobile apps both consume the same package, so endpoint shapes, payloads, and response types stay aligned with the FastAPI contract.
- Docker Desktop
- Docker Compose
- Node.js 22+
- pnpm 10+
docker compose up --build- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- Health check: http://localhost:8000/health
- Web app:
pnpm dev:web - Mobile app:
pnpm dev:mobile
docker compose downThe repository now includes a root railway.toml so Railway has an explicit deploy config:
- build from the root
Dockerfile - run
alembic upgrade headas a pre-deploy step - start Uvicorn with Railway's injected
PORT - healthcheck
GET /health - exclude the local
.envfrom Docker builds so Railway uses service variables instead of development defaults
If you want to verify or override the service manually in Railway UI:
sh -c "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"Recommended build command:
pip install -r requirements.txtThe repository also includes a Procfile as a fallback, but Railway should prefer railway.toml.
Minimum variables for a web deployment:
DATABASE_URL,DATABASE_PUBLIC_URL,POSTGRES_URL,POSTGRES_URL_NON_POOLING,POSTGRES_PRISMA_URL,POSTGRESQL_URL, or Railway Postgres variablesPGHOST,PGPORT,PGUSER,PGPASSWORD, andPGDATABASESECRET_KEYfor stable JWT signing
Optional variables:
REDIS_URLfor cache and Celery on RailwayCELERY_BROKER_URLandCELERY_RESULT_BACKENDif you want to override the defaults explicitly
Recommended Railway Redis variables on the web service:
REDIS_URL=${{Redis.REDIS_URL}}
CELERY_BROKER_URL=${{Redis.REDIS_URL}}
CELERY_RESULT_BACKEND=${{Redis.REDIS_URL}}
If SECRET_KEY is omitted, the app now generates an ephemeral key at boot so the service can start, but existing auth tokens will become invalid after every restart.
If Postgres is temporarily unavailable during startup, the FastAPI service now retries database readiness checks with bounded backoff before failing the boot.
If Redis variables are omitted, the web process can still boot, but it now skips Redis connection attempts and returns a clear 503 for background-job requests until a Redis service is configured.
If Redis is configured but slow to answer during startup, the web process now gives up after a short timeout and keeps booting without cache instead of hanging the whole deploy.
The backend now always allows the stable frontend origins https://atlhyt.com, https://www.atlhyt.com, and https://gym-app-back-web.vercel.app by default. Any CORS_ORIGINS value you set adds extra origins instead of replacing those defaults. For Vercel preview deployments, you can additionally set CORS_ORIGIN_REGEX.
Important Railway UI check:
- open your web service, then
Variables - make sure the database variables are present on that web service itself
- if you created a Railway Postgres service, link or reference its variables into the web service before redeploying
docker compose down -vThe repository includes a ready-to-use backend .env for local development and a matching .env.example.
Key variables:
SECRET_KEY: JWT signing secretDATABASE_URL: async SQLAlchemy database URLDATABASE_CONNECT_TIMEOUT_SECONDS: timeout for each database readiness probeDATABASE_STARTUP_MAX_ATTEMPTS: number of startup probe attempts before boot failsDATABASE_STARTUP_INITIAL_BACKOFF_SECONDS: initial delay between startup probesDATABASE_STARTUP_MAX_BACKOFF_SECONDS: cap for startup retry delayDATABASE_STARTUP_BACKOFF_MULTIPLIER: backoff multiplier applied between attemptsREDIS_URL: Redis cache URLCORS_ORIGINS: extra allowed frontend origins added on top of the built-in stable frontend originsCORS_ORIGIN_REGEX: optional regex for preview deployments such as Vercel branch URLsCELERY_BROKER_URL: Redis broker URL for CeleryCELERY_RESULT_BACKEND: result backend URL for CeleryVITE_API_URL: base URL used byapps/webEXPO_PUBLIC_API_URL: base URL used byapps/mobileSTRAVA_CLIENT_ID: optional Strava OAuth client idSTRAVA_CLIENT_SECRET: optional Strava OAuth client secret
The app uses Google Identity Services with a popup/token flow (not redirect flow).
Frontend (apps/web/.env.local):
VITE_API_URL=https://your-backend-url.com
VITE_GOOGLE_CLIENT_ID=your-web-client-id.apps.googleusercontent.comNote:
.env.localis gitignored and loaded at runtime. Use.env.exampleas a template only.
Backend (.env):
GOOGLE_CLIENT_ID=your-web-client-id.apps.googleusercontent.com
# Optional: add mobile client ID as well
GOOGLE_CLIENT_IDS=your-web-client-id.apps.googleusercontent.com,your-mobile-client-id.apps.googleusercontent.com-
Go to Google Cloud Console
-
Select your project
-
Navigate to APIs & Services > Credentials
-
Create or select an OAuth 2.0 Client ID (Web application type)
-
Add Authorized JavaScript origins:
http://localhost:5173(local development)https://your-production-domain.com(production)
-
No redirect URI is required for Google Identity Services popup flow
See docs/google-auth-setup.md for complete mobile setup documentation.
- Frontend loads Google Identity Services script
- User clicks "Sign in with Google" button
- GSI shows popup, user authenticates with Google
- GSI returns
id_tokendirectly to frontend (no redirect) - Frontend sends
id_tokentoPOST /auth/google-login - Backend validates token against configured
GOOGLE_CLIENT_ID - Backend issues app JWT and creates member if needed
Authentication uses bearer tokens.
POST /auth/registerPOST /auth/loginPOST /auth/google-login(Google Identity Services popup flow)
Members are explicitly categorized by authentication origin:
| Provider | Description |
|---|---|
local |
Registered with email + password |
google |
Authenticated via Google Sign-In |
The auth_provider field on the Member model distinguishes the authentication origin. This replaces the previous implicit approach of using empty password hash for Google users.
Google authentication uses the stable Google sub (subject) identifier for account linkage.
google_sub stores only the real Google subject from verified tokens. It is never synthetic. Legacy Google accounts (created before this feature) may have google_sub=NULL temporarily; the first successful Google login backfills the verified sub.
Linking flow:
- Same
google_subexists: Login succeeds - No
google_submatch, but email exists:auth_provider=google,google_sub=NULL→ Backfill realsubfrom verified token, login succeedsauth_provider=local,google_sub=NULL→ Link Google by storing realsub, preserve password, login succeedsgoogle_subalready linked to different account → Reject (conflict)
- No account exists: Auto-provision new member with real
google_sub
- Local login requires valid password; fails for Google-only accounts
- Google login requires verified email and valid
subclaim google_subis always from a verified Google token (never synthetic)- Account linking is conservative: same Google identity cannot be reassigned to another email
- Legacy accounts may have
google_sub=NULLuntil first successful Google login
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_IDS=web-client-id,mobile-client-id # optional: allow multipleSupported member roles:
memberinstructoradmin
Examples:
- admins can manage members and classes
- instructors can manage their own classes and mark attendance
- members can manage their own bookings, integrations, and profile data
POST /membersGET /membersGET /members/{id}PATCH /members/{id}
POST /classesGET /classesGET /classes/{id}PATCH /classes/{id}DELETE /classes/{id}
POST /bookingsDELETE /bookings/{id}GET /members/{id}/bookings
POST /attendanceGET /attendance/class/{classId}GET /attendance/member/{memberId}
POST /integrations/strava/connectPOST /activities/syncGET /members/{id}/activities
Booking is designed to be concurrency-safe.
The service layer uses:
- database transactions
- row-level locking on the class row
- booking count checks inside the transaction
- automatic waitlist promotion after cancellation
This prevents overbooking when multiple users reserve the same class at the same time.
Celery services included in Docker Compose:
celery_workercelery_beat
Registered tasks:
app.tasks.sync_member_activitiesapp.tasks.send_class_reminders
Alembic is configured and the initial schema migration is included.
Run migrations manually if needed:
alembic upgrade headCreate a new autogenerated migration:
alembic revision --autogenerate -m "describe_change"Redis is used for:
- Celery message brokering
- Celery result storage
- caching frequently requested member and class detail responses
Run the API locally outside Docker:
uvicorn app.main:app --reloadRun the worker locally:
celery -A app.workers.celery_app.celery_app worker --loglevel=infoRun beat locally:
celery -A app.workers.celery_app.celery_app beat --loglevel=infoInspect running containers:
docker compose psShow logs:
docker compose logs -f api
docker compose logs -f celery_worker
docker compose logs -f celery_beat- Railway continues to deploy the backend from the repository root.
- Vercel can target
apps/webfor the web client. - Expo EAS is configured in
apps/mobile/eas.jsonfor mobile releases. - GitHub Actions frontend CI is available in
.github/workflows/frontend-ci.ymland runs API generation, typechecking, linting, and both app builds.
Prepare the Vercel project with these settings:
- Framework Preset:
Vite - Root Directory:
apps/web - Node.js Version:
22.x - Install Command: leave default to use
apps/web/vercel.json, or setcd ../.. && pnpm install --frozen-lockfile - Build Command: leave default to use
apps/web/vercel.json, or setcd ../.. && pnpm vercel:build:web - Output Directory:
dist
Required environment variable:
VITE_API_URL=https://gymappback-production-7f4e.up.railway.app
The web app’s Vercel project config is committed at apps/web/vercel.json, so the monorepo can be deployed from this repository without moving the FastAPI backend out of the root.
- The Strava connect endpoint stores access credentials supplied by a client-side OAuth flow or admin tooling.
- The class model file is named
gym_class.pybecauseclass.pywould conflict with Python syntax. - The local Docker stack has been verified to serve
/healthand/docs.
Contributions are welcome! Please read our Contributing Guidelines and Code of Conduct to get started.
This project is licensed under the MIT License - see the LICENSE file for details.