TechSync Ops is a multi-tenant maintenance command platform for property management companies (PMCs) and field-service teams that need intake, dispatch, technician proof, client communication, reporting, and operational accountability in one place.
This repository implements the POC scope defined in
TECHSYNC_OPS_REQUIREMENTS.md and the v1.2/v1.3 path in
PRODUCT_ROADMAP.md. See Spec Coverage below for what's
implemented vs. deferred.
TechSync Ops ingests maintenance work orders from CSV and webhook sources, validates and normalizes them, assigns them to the best-fit technician based on skills, proximity, priority, and workload, then tracks status, proof, and audit history through completion. Every organization (tenant) gets its own isolated slice of data, enforced in the application layer and backed by Postgres Row Level Security policies.
Client
- React Native (Expo-managed, RN 0.73.6)
- React Navigation
Backend
- FastAPI + Uvicorn
- Managed PostgreSQL, accessed directly through SQLAlchemy/psycopg2 repositories
- S3-compatible object storage for work order attachments (Cloudflare R2, AWS S3, etc.)
- Pydantic v2 for request/response validation
- Alembic for versioned migrations
- JWT (access + refresh) for auth, bcrypt for password hashing
.
├── client/ # React Native mobile application
│ └── src/
│ ├── context/AuthContext.js # tokens, onboarding, invites, refresh
│ ├── screens/ # Login, Onboarding, Invite, Password reset,
│ │ # Work order list/details/form
│ └── config.js
└── server/ # FastAPI backend
├── main.py # app wiring: routers, CORS, exception handlers
├── core/ # config.py, security.py (JWT, hashing)
├── models/ # Pydantic request/response schemas
├── repositories/ # direct Postgres data access, always org-scoped
├── services/ # business logic (matching, ingestion, billing...)
├── routers/ # HTTP endpoints, one file per resource
├── dependencies.py # auth, tenant-scoping, role-check dependencies
├── alembic/ # versioned DB migrations (RNF-10)
├── schema.sql # same schema, for managed Postgres setup
├── tests/ # pytest suite (auth, matching, tenant isolation...)
├── Dockerfile / .dockerignore
└── requirements.txt
Every tenant-scoped table (users, technicians, work_orders,
work_order_events, work_order_attachments, invitations,
org_priority_rules) carries an organization_id column.
Two enforcement layers:
- Application layer (primary for this POC) — every function in
server/repositories/*.pytakes anorganization_idand filters on it explicitly.server/tests/test_tenant_isolation.pyasserts this for the core repositories.server/dependencies.py::get_current_organizationresolves the caller's org from their JWT and every router depends on it. - Row Level Security (backstop) — every tenant table has RLS enabled
with a policy scoping rows to
techsync_current_org_id(), which reads anorganization_idclaim from the database session. This was manually verified against a local Postgres instance during development: with RLS on and no org claim set, queries return zero rows (fail-closed); with the claim set to org A, only org A's rows are visible, even though org B's rows exist in the same table.
Direct runtime model: the FastAPI backend now talks directly to Postgres with DATABASE_URL; there is no Supabase service-role runtime dependency. Application-layer organization_id scoping is the primary tenant boundary and is covered by repository regression tests. RLS policies remain in the schema as a database backstop for deployments that intentionally set a per-request organization_id claim in the database session.
- Node.js 16+
- Python 3.11+
- A managed Postgres database (Neon, Render, Railway, Fly, local Postgres, etc.)
- Android Studio or Xcode for mobile development
cd server
pip install -r requirements.txt
cp .env.example .envEdit .env:
APP_ENV=development
DATABASE_URL=postgresql://user:password@host:5432/techsync
JWT_SECRET_KEY=$(openssl rand -hex 32)
CORS_ORIGINS=http://localhost:8081,http://localhost:19006,http://localhost:3000
STORAGE_BUCKET=work-order-attachments
STORAGE_REGION=auto
STORAGE_ENDPOINT_URL=https://your-account-id.r2.cloudflarestorage.com
STORAGE_ACCESS_KEY_ID=your-storage-access-key
STORAGE_SECRET_ACCESS_KEY=your-storage-secret-key
STORAGE_PUBLIC_BASE_URL=https://files.yourdomain.com/work-order-attachments
ATTACHMENT_MAX_BYTES=10485760
STRIPE_SECRET_KEY=
STRIPE_PRICE_ID=
STRIPE_WEBHOOK_SECRET=whsec_your_stripe_webhook_secret
STRIPE_SUCCESS_URL=http://localhost:3000/billing/success
STRIPE_CANCEL_URL=http://localhost:3000/billing/cancel
APP_BASE_URL=http://localhost:19006
EMAIL_DELIVERY_METHOD=log
EMAIL_FROM=TechSync <no-reply@yourdomain.com>
For the first hosted investor POC, start from server/.env.demo.example
instead. APP_ENV=demo still requires hosted HTTPS URLs, a real DATABASE_URL,
JWT_SECRET_KEY, and locked-down CORS_ORIGINS, but it intentionally permits
SMTP email, Cloudflare R2/S3 attachment storage, and Stripe keys to stay empty.
Use APP_ENV=production only after SMTP and object storage are configured.
Apply the schema with Alembic (recommended), or run schema.sql in your managed Postgres SQL console (RNF-10):
alembic upgrade headRun the server:
uvicorn main:app --reloadAPI docs: http://localhost:8000/docs
cd server
pip install -r requirements-dev.txt
pytest -p no:cacheprovider70 backend tests covering JWT/password logic, configuration validation, the matching engine, CSV ingestion validation, work order status transitions, plan-limit enforcement, tenant-isolation of the repository layer, public endpoint rate limiting, and Stripe webhook handling, and attachment upload validation. These run without a live database (repositories are mocked); the RLS behavior described above was additionally verified by hand against a local Postgres instance.
The GitHub Actions workflow also runs this backend suite plus the client Jest
checks on pushes to main / agent/** and pull requests into main.
docker compose up --buildBuilds server/Dockerfile and serves the API on port 8000, reading
server/.env. (Note: the container image build itself wasn't network-
reachable to pull python:3.11-slim in the sandboxed environment this was
developed in — the Dockerfile follows standard, well-tested patterns but
verify the build in your own environment before relying on it.)
cd client
npm install
npm start
# in another terminal:
npm run android # or: npm run iosUpdate src/config.js if your backend isn't on localhost:8000 (Android
emulator needs http://10.0.2.2:8000; physical devices need your
machine's LAN IP). Native builds persist auth tokens with expo-secure-store
(OS Keychain/Keystore-backed storage); Expo web preview uses browser session
storage or memory because browsers do not expose the same secure store.
There's no shared "register" endpoint anymore — a brand-new company signs
up via POST /organizations/onboard, which creates the organization and
its first org_admin user in a single call and returns tokens
immediately. Everyone else joins via an emailed invitation
(POST /organizations/invitations → POST /invitations/accept, RF-07). In
the mobile app: "Create Organization" on the login screen for the first
flow, "Accept Invitation" for the second.
Full interactive docs at /docs. Summary:
| Area | Endpoints |
|---|---|
| Auth (RF-01, RF-03) | POST /auth/login, POST /auth/refresh, POST /auth/forgot-password, POST /auth/reset-password, GET /auth/me |
| Organizations (RF-05, RF-06, RF-08, RNF-13) | POST /organizations/onboard, GET/PATCH /organizations/me, POST /organizations/me/api-key/regenerate, DELETE /organizations/me |
| Invitations (RF-07) | POST/GET /organizations/invitations, POST /invitations/accept |
| Users (RF-02) | GET /users, PATCH /users/{id}/role |
| Technicians (RF-26, RF-29) | POST/GET /technicians, PATCH /technicians/{id} |
| Work Orders (RF-14, RF-15, RF-18..RF-22, RF-24) | POST/GET /work-orders, GET /work-orders/mine, GET/PATCH /work-orders/{id}, PATCH /work-orders/{id}/status, POST /work-orders/{id}/assign, GET /work-orders/{id}/events, POST /work-orders/{id}/attachments/upload, POST/GET /work-orders/{id}/attachments |
| Ingestion (RF-09, RF-11, RF-12) | POST /ingestion/csv (multipart), POST /ingestion/webhook (X-API-Key header, per-org key) |
| Dashboard (RF-25) | GET /dashboard/metrics |
| Billing (RF-27, RF-28, RF-29) | POST /billing/checkout, POST /billing/webhook, GET /billing/plan-limits |
Access token lifetime is 15 minutes, refresh token 7 days (RF-01). Roles are
org_admin, coordinator, technician (RF-02), enforced per-endpoint via
dependencies.require_roles(...).
Public auth-style endpoints also have configurable fixed-window rate limits
(RATE_LIMIT_* environment variables) for single-instance POC hosting. If the
backend is scaled horizontally, enforce equivalent limits through Redis or the
edge/reverse proxy so counters are shared across instances.
# 1. Create an organization + admin
curl -s -X POST http://localhost:8000/organizations/onboard \
-H "Content-Type: application/json" \
-d '{"company_name":"Acme Field","admin_full_name":"Jane Admin","admin_email":"jane@acme.com","admin_password":"DemoPass123"}'
# -> { "organization": {...}, "user": {...}, "tokens": {"access_token": "...", "refresh_token": "..."} }
TOKEN="paste access_token here"
# 2. Create a work order (auto-assigns a technician if one is eligible)
curl -s -X POST http://localhost:8000/work-orders \
-H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-d '{"title":"Fix leak","service_type":"plumbing","priority":"high"}'
# 3. Bulk-ingest via CSV
curl -s -X POST http://localhost:8000/ingestion/csv \
-H "Authorization: Bearer $TOKEN" -F "file=@work_orders.csv"Implemented for this POC pass (mapped to TECHSYNC_OPS_REQUIREMENTS.md):
- Auth & users: RF-01 (access+refresh JWT), RF-02 (3 roles + middleware), RF-03 (password reset flow), RF-04 (native token storage via Expo SecureStore).
- Multi-tenancy: RF-05 (org_id scoping + RLS), RF-06 (self-service onboarding), RF-07 (invitations), RF-08 (org settings: timezone, service types, priorities).
- Ingestion: RF-09 (CSV), RF-11 (webhook, API-key auth), RF-12 (Pydantic validation, per-row errors). RF-10 (PDF/web form extraction) and RF-13 (email ingestion) are deferred per the spec's own scope note (Should/Could, not blocking).
- Matching: RF-14 (skills + proximity + workload scoring engine, see
services/matching_service.py), RF-15 (manual reassignment), RF-16 (notification service — logs a structured event; no real push infrastructure, documented inservices/notification_service.py), RF-17 (per-org forced-priority rules). - Work orders: RF-18 (CRUD + enforced status transitions), RF-19 (attachment metadata plus backend upload endpoint for S3-compatible object storage when configured), RF-20 (audit log), RF-21 (filtered search).
- Mobile: RF-22 (technician's assigned queue, ordered by priority), RF-24 (status update with notes). RF-23 (offline sync) is deferred per spec scope note.
- Admin panel: RF-26 (technician CRUD), RF-25 (dashboard metrics endpoint; polling from a web admin panel is not built in this pass — see Known Gaps).
- Billing: RF-27 (14-day trial default), RF-28 (Stripe Checkout in test mode, signed Stripe webhook handling for checkout completion/payment failure/subscription cancellation, or a mock URL when Stripe is not configured), RF-29 (technician-count plan limit, enforced server-side).
- NFRs: RNF-05 (application-layer tenant isolation with repository regression tests; RLS policies remain as an optional DB backstop),
RNF-06 (bcrypt), RNF-09 (modular backend structure), RNF-10 (Alembic),
RNF-11 (Dockerfile/compose), RNF-12 (structured JSON logging, toggle via
LOG_FORMAT=json), RNF-13 (tenant deletion endpoint).
-
No web admin panel was built (RF-25/RF-26 exist as API endpoints only); the spec's "panel administrativo" is assumed to be a future separate web client consuming this same API.
-
PMC operations expansion: first-class properties, clients/homeowners, vendors, approvals, client-visible communication, closeout packages, SLA risk, duplicate detection, calendar/maps, and operational exports are tracked for v1.3+ in
PRODUCT_ROADMAP.md. -
RF-23 (offline sync), RF-10/RF-13 (PDF/email ingestion): deferred, per the spec's own "Notas de Alcance" — not blocking for a POC.
-
Client dependency audit:
npm audit fix --package-lock-onlyhas been applied without forcing framework majors. The remaining npm findings are in Expo 50 / React Native 0.73 transitive tooling and require a planned Expo/RN upgrade before treating the client as public-showcase clean. -
Docker image build was not network-testable in the sandbox this was built in (registry pull blocked); verify the image build in your deployment environment before relying on it.
MIT