Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions BUILD_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ docs also advertised those credentials.
### Remaining Follow-Up

- Run a secret/history scan before making the repo public.
- If any Supabase project was already initialized with the previous schema,
manually remove or rotate the former shared demo users there.
- If any existing database was already initialized with the previous schema,`n manually remove or rotate the former shared demo users there.

## 2026-07-09 - Make Production Configuration Explicit

Expand All @@ -41,8 +40,7 @@ hosted POC fail in confusing ways or accidentally point users at fake domains.
in production builds when it is missing.
- Backend settings now support `APP_ENV=production` and validate required
production values at startup.
- Production backend startup now requires Supabase settings, JWT secret, CORS
origins, and Stripe/mock checkout callback URLs.
- Production backend startup now requires `DATABASE_URL`, object storage settings,`n JWT secret, CORS origins, and Stripe/mock checkout callback URLs.
- Production CORS validation rejects localhost origins.
- `.env.example`, README, Quick Start, and mobile config docs now describe the
deployment variables explicitly.
Expand Down Expand Up @@ -191,15 +189,39 @@ end-to-end.

### Changed

- Added configurable Supabase Storage settings for the work-order attachments bucket and max upload size.
- Added a backend upload service that validates image/PDF content type, size, and filename safety before uploading to Supabase Storage.
- Added configurable S3-compatible storage settings for the work-order attachments bucket and max upload size.
- Added a backend upload service that validates image/PDF content type, size, and filename safety before uploading to S3-compatible object storage.
- Added `POST /work-orders/{id}/attachments/upload` while keeping the existing metadata endpoint compatible.
- Added backend tests for upload path generation, Supabase Storage calls, content-type rejection, size rejection, and extension mismatch rejection.
- Added backend tests for upload path generation, S3-compatible storage calls, content-type rejection, size rejection, and extension mismatch rejection.
- Added `expo-image-picker` plus camera/photo-library permissions to the mobile app.
- Updated the work order details screen to load attachments, take or choose a photo, upload it, and open existing attachments.
- Updated README and the pre-launch checklist to mark RF-19 complete for the hosted POC path.

### Remaining Follow-Up

- Create the `work-order-attachments` Supabase Storage bucket in the production project before the hosted demo.
- Create the `work-order-attachments` bucket in the chosen object-storage provider before the hosted demo.
- Smoke-test photo capture/upload on a real Android/iOS build after the next native rebuild.
## 2026-07-10 - Remove Supabase Runtime Dependency

### Why

Supabase was no longer the chosen foundation for the hosted POC. The backend
already owned auth, tenant scoping, migrations, and file upload endpoints, so a
portable managed-Postgres plus S3-compatible storage architecture is a better
fit for the public demo path.

### Changed

- Added `server/database.py` for direct SQLAlchemy/psycopg2 Postgres access via `DATABASE_URL`.
- Migrated runtime repositories from `supabase-py` query-builder calls to explicit Postgres SQL/helper calls.
- Replaced `supabase` with `boto3` in backend requirements.
- Reworked attachment uploads to use S3-compatible `put_object` storage and public base URLs.
- Removed the obsolete `server/supabase_client.py` runtime helper.
- Updated tenant-isolation tests to assert direct SQL/helper scoping by `organization_id`.
- Updated `.env.example`, README, and the pre-launch checklist for managed Postgres plus S3/R2-style storage.

### Remaining Follow-Up

- Choose the hosted providers (for example Neon/Render Postgres plus Cloudflare R2) and provision real credentials.
- Run `alembic upgrade head` against the production/demo Postgres database.
- Smoke-test attachment upload against the chosen object-storage bucket.
26 changes: 6 additions & 20 deletions PRE_LAUNCH_CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ before real customer data goes in, **Nice-to-have** can trail behind launch.
-hex 32`) — never reuse the one from local dev, never commit it.
Store it in your hosting provider's secret manager, not in a
committed `.env`.
- [ ] **Create a production Supabase project** (separate from any dev/test
project) and run `alembic upgrade head` (or paste `schema.sql`)
against it. Use the `service_role` key only on the backend server,
never ship it to the mobile app or a browser.
- [ ] **Create a production managed Postgres database** (separate from any dev/test database) and run `alembic upgrade head` against it. Keep `DATABASE_URL` only in the backend host secret manager.
- [ ] **Put the backend behind HTTPS** (RNF-04 requires TLS, no
exceptions). If you deploy to Render/Fly.io/Railway/Vercel this is
usually automatic; if you're running the Docker image yourself, put
Expand All @@ -33,7 +30,7 @@ before real customer data goes in, **Nice-to-have** can trail behind launch.
requires SMTP settings and `APP_BASE_URL` at startup.
- [ ] **Run a secret scan** over the whole repo history before making it
public (`git log` + tools like `gitleaks` or `trufflehog`) — confirm
no real Supabase/Stripe/JWT keys were ever committed, only the
no real database/storage/Stripe/JWT keys were ever committed, only the
`.env.example` placeholders.

## 🟠 Important — do before real customer data / real money touches this
Expand All @@ -47,19 +44,9 @@ before real customer data goes in, **Nice-to-have** can trail behind launch.
`/invitations/accept`). The backend now has configurable in-process fixed
window limits for single-instance POC hosting. For multi-instance hosting,
move these limits to Redis or the edge/reverse proxy.
- [ ] **Decide on the RLS/service-role gap.** The backend currently uses
the Supabase `service_role` key, which bypasses Row Level Security —
application-layer `organization_id` scoping is the real enforcement
today (see README "Multi-Tenancy Model"). Before this holds real
customer data, either (a) accept and document that app-layer
scoping is your primary control and add automated regression tests
that fail loudly if a repository function ever drops its
`organization_id` filter, or (b) do the work to issue
PostgREST-compatible JWTs with an `organization_id` claim and switch
reads to the `anon`/`authenticated` key so RLS is actually live in
production, not just verified manually in dev.
- [x] **Remove the Supabase service-role runtime dependency.** Repositories now use direct Postgres via `DATABASE_URL`, storage uses S3-compatible credentials, and tenant isolation is enforced through app-layer `organization_id` scoping covered by regression tests.
- [x] **Wire up real object storage for attachments (RF-19).** The backend
now uploads files through Supabase Storage and records attachment
now uploads files through S3-compatible object storage and records attachment
metadata; the mobile work order details screen can take or choose a
photo and attach it to the job.
- [x] **Move mobile token storage off AsyncStorage** (RF-04). Native app
Expand All @@ -72,12 +59,11 @@ before real customer data goes in, **Nice-to-have** can trail behind launch.
`LOG_FORMAT=json` logs, which nobody's watching in real time.
- [ ] **Set up basic uptime monitoring** (UptimeRobot, Better Uptime, or
your host's built-in health checks against `GET /health`).
- [ ] **Confirm Supabase automated backups are enabled** on the production
- [ ] **Confirm managed Postgres automated backups are enabled** on the production
project (paid tiers include point-in-time recovery — check your plan).
- [x] **Add a CI pipeline** (GitHub Actions) that runs backend pytest and
client Jest checks on pushes/PRs. Backend coverage currently includes
`server/tests/` (58 tests); client coverage starts with shared validation
tests and should grow as mobile flows are hardened.
`server/tests/` (63 tests); client coverage starts with shared validation tests and should grow as mobile flows are hardened.
- [ ] **Complete the Expo/React Native dependency upgrade.** Safe npm audit
fixes reduced the client report from 39 to 29 findings with no criticals,
but the remaining high/moderate findings are pinned inside Expo 50 / React
Expand Down
25 changes: 9 additions & 16 deletions QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@

## 5-Minute Demo Script

Since TechSync is now multi-tenant, the backend requires a real Supabase
project (there's no more "run with no DB, get mock data" mode — tenant
isolation needs a real database). Budget a couple extra minutes the first
time to create a free Supabase project and run the schema.
Since TechSync is multi-tenant, the backend requires a real Postgres database for runtime data. Budget a few minutes to create a managed Postgres database (Neon, Render, Railway, Fly, or local Postgres) and run the schema.

### Step 0: One-time Supabase setup
### Step 0: One-time database setup

1. Create a free project at https://supabase.com
2. Open the SQL Editor and paste/run the contents of `server/schema.sql`
(or run `alembic upgrade head` from `server/` with `DATABASE_URL` set to
your project's direct Postgres connection string)
3. Grab your Project URL and `service_role` key from Project Settings → API
1. Create a managed Postgres database.
2. Set `DATABASE_URL` to that database connection string.
3. Run `alembic upgrade head` from `server/`.

### Step 1: Start the Backend (1 minute)

Expand All @@ -23,8 +18,7 @@ pip install -r requirements.txt

cat > .env << EOF
APP_ENV=development
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-service-role-key
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
STRIPE_SUCCESS_URL=http://localhost:3000/billing/success
Expand Down Expand Up @@ -109,7 +103,7 @@ ship with shared demo credentials.
## Key Features to Showcase

1. **Multi-tenant isolation** — every table is scoped by `organization_id`,
backed by Postgres RLS (see README "Multi-Tenancy Model")
backed by application-layer tenant scoping plus the schema RLS backstop (see README "Multi-Tenancy Model")
2. **Self-service onboarding** — a brand-new org + admin account in one API
call, no manual provisioning
3. **Rule-based auto-assignment** — new work orders are matched to a
Expand Down Expand Up @@ -142,9 +136,8 @@ cd client && npm start -- --reset-cache
- Physical Device: `http://YOUR_COMPUTER_IP:8000`

**"503 Service requires database configuration":**
- `SUPABASE_URL`/`SUPABASE_KEY` are missing or wrong in `server/.env` — see
Step 0 above. There's no mock-data fallback anymore since tenant
isolation needs a real database.
- `DATABASE_URL` is missing or wrong in `server/.env` - see Step 0 above.
There's no mock-data fallback anymore since tenant isolation needs a real database.

**Access token expired errors:**
- The mobile app refreshes automatically. Via curl, call
Expand Down
51 changes: 20 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A multi-tenant SaaS platform for field service companies to onboard their
organization, ingest work orders from multiple sources, auto-assign them to
the right technician, and track them to completion — with a React Native
mobile app for technicians and a FastAPI + Supabase backend.
mobile app for technicians and a FastAPI + managed Postgres backend.

This repository implements the POC scope defined in
`Techsync_SaaS_Requirements.md` (Functional/Non-Functional requirements
Expand All @@ -27,8 +27,8 @@ and at the database layer via Postgres Row Level Security.

**Backend**
- FastAPI + Uvicorn
- Supabase (PostgreSQL), accessed via `supabase-py` (PostgREST) at runtime
and via a direct Postgres connection for Alembic migrations
- 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
Expand All @@ -47,12 +47,12 @@ and at the database layer via Postgres Row Level Security.
├── main.py # app wiring: routers, CORS, exception handlers
├── core/ # config.py, security.py (JWT, hashing)
├── models/ # Pydantic request/response schemas
├── repositories/ # Supabase data access, always org-scoped
├── 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 pasting into Supabase SQL editor
├── schema.sql # same schema, for managed Postgres setup
├── tests/ # pytest suite (auth, matching, tenant isolation...)
├── Dockerfile / .dockerignore
└── requirements.txt
Expand All @@ -72,30 +72,21 @@ Two enforcement layers:
core repositories. `server/dependencies.py::get_current_organization`
resolves the caller's org from their JWT and every router depends on it.
2. **Row Level Security (backstop)** — every tenant table has RLS enabled
with a policy scoping rows to `techsync_current_org_id()`, which reads an
`organization_id` claim off the PostgREST JWT. This was manually verified
with a policy scoping rows to `techsync_current_org_id()`, which reads an `organization_id` claim 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.

**Caveat**: the backend currently talks to Supabase using the
`service_role` key (needed for cross-tenant admin operations like
onboarding), which bypasses RLS by design in Supabase. That makes the
application-layer scoping the actual enforcement path today. RLS is fully
defined and tested so that the moment any code path uses the `anon` key
with a user-scoped JWT (e.g. a future "mobile app talks to Supabase
directly" path), it's already protected — see the comment block at the top
of `server/schema.sql` for how to mint a PostgREST-compatible JWT with an
`organization_id` claim.
**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.

## Getting Started

### Prerequisites

- Node.js 16+
- Python 3.11+
- A Supabase project (or local Postgres for schema/migration testing)
- A managed Postgres database (Neon, Render, Railway, Fly, local Postgres, etc.)
- Android Studio or Xcode for mobile development

### Backend Setup
Expand All @@ -109,11 +100,16 @@ cp .env.example .env
Edit `.env`:
```
APP_ENV=development
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-service-role-key
DATABASE_URL=postgresql://postgres:password@db.your-project.supabase.co:5432/postgres
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
Expand All @@ -122,12 +118,9 @@ 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>
SUPABASE_ATTACHMENTS_BUCKET=work-order-attachments
ATTACHMENT_MAX_BYTES=10485760
```

Apply the schema — either paste `schema.sql` into the Supabase SQL editor,
or run the equivalent migration (RNF-10):
Apply the schema with Alembic (recommended), or run `schema.sql` in your managed Postgres SQL console (RNF-10):
```bash
alembic upgrade head
```
Expand All @@ -145,7 +138,7 @@ cd server
pip install -r requirements-dev.txt
pytest -p no:cacheprovider
```
62 backend tests covering JWT/password logic, the matching engine, CSV ingestion
63 backend tests covering JWT/password logic, 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
Expand Down Expand Up @@ -270,7 +263,7 @@ Implemented for this POC pass (mapped to `Techsync_SaaS_Requirements.md`):
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 (RLS, verified manually — see Multi-Tenancy Model above),
- **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).
Expand All @@ -287,11 +280,7 @@ Implemented for this POC pass (mapped to `Techsync_SaaS_Requirements.md`):
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 wasn't network-testable in the sandbox this was built
in (registry pull blocked); the Alembic migration itself *was* run
end-to-end against a real local Postgres instance and confirmed to create
the correct schema, indexes, and RLS policies, and RLS behavior was
hand-verified with a non-superuser role.
- 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.

## License

Expand Down
Loading
Loading