Decentralized P2P Escrow Β· Stellar Blockchain Β· MVP monorepo
dApp-SafeTrust is the MVP integration monorepo β it wires
apps/frontend(Next.js 14) andapps/api(Node.js + Express) together through a Hasura GraphQL middleware and a PostgreSQL database. One command starts everything.
π Quick Start Β· ποΈ Architecture Β· π Structure Β· π§© Apps & Packages Β· π οΈ Development Β·
SafeTrust is a decentralized platform for secure P2P rental transactions. It holds funds in tamper-proof blockchain escrow contracts on the Stellar network via the TrustlessWork API β no intermediaries, no hidden fees, full on-chain transparency.
The core MVP flow:
Tenant finds property β clicks PAY β connects Freighter wallet
β escrow deployed on Stellar β funds locked until agreement fulfilled
β owner receives funds on release Β· tenant recovers deposit on dispute
dApp-SafeTrust uses a decoupled, API-first architecture. Hasura acts as the middleware β it auto-generates a GraphQL API from the database tables, eliminating hand-written REST controllers for data reads.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Stellar Blockchain β
β (TrustlessWork API) β
ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β HTTP (signed XDR)
ββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ
β services/webhook (Node.js + Express) β
β Firebase Auth sync Β· POST /api/auth/sync-user β
β Runs inside Docker as safetrust-webhook β
ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β SQL (pg pool)
ββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ
β infra/hasura (MIDDLEWARE) β
β Auto-generated GraphQL API Β· JWT validation β
β Tables: users Β· escrows Β· bid_requests Β· milestones β
β Runs on port 8080 β
ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β GraphQL (queries Β· mutations Β· subscriptions)
ββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ
β apps/frontend (Next.js 14) β
β Apollo Client Β· Firebase Auth Β· Freighter Wallet β
β Runs on port 3001 β
β β
β /login β /register β /dashboard β PAY β escrow live β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key principle: apps/frontend never talks to services/webhook directly for data reads. All reads go through Hasura GraphQL. The webhook service only handles write-heavy operations (auth sync, escrow deploy) that need business logic.
dApp-SafeTrust/
βββ apps/
β βββ frontend/ β Next.js 14 (App Router) Β· port 3001
β β βββ src/
β β β βββ app/
β β β β βββ layout.tsx β root layout β wraps ClientProviders
β β β β βββ login/ β /login page
β β β β βββ register/ β /register page
β β β β βββ dashboard/ β /dashboard page
β β β βββ components/
β β β β βββ auth/ β Login.tsx Β· Register.tsx Β· wallet modals
β β β β βββ escrow/ β EscrowPayFlow Β· ProcessStepper
β β β β βββ ui/ β shadcn/ui primitives (all import from @/lib/utils)
β β β βββ config/
β β β β βββ apollo.ts β Apollo Client Β· authLink (Firebase JWT)
β β β βββ lib/
β β β β βββ firebase.ts β Firebase client SDK init
β β β β βββ subscription-client.ts β WebSocket client for subscriptions
β β β β βββ utils.ts β cn() helper (clsx + tailwind-merge)
β β β β βββ walletconnect.ts β WalletConnect URI stub
β β β βββ graphql/
β β β β βββ queries/
β β β β βββ user-queries.ts β GET_USER query
β β β βββ providers/
β β β βββ ClientProviders.tsx β "use client" wrapper
β β β βββ ApolloProviderWrapper.tsx β ApolloProvider with apolloClient
β β β βββ TrustlessWorkProvider.tsx β Trustless Work SDK context
β β βββ codegen.ts β graphql-codegen config
β β βββ package.json β @safetrust/web
β β
β βββ api/ β Node.js + Express Β· port 3000
β βββ src/
β βββ index.js β Express server entry point
β βββ lib/
β β βββ trustlesswork.js β TrustlessWork API client
β βββ routes/
β βββ escrow/ β deploy.handler.js Β· deploy.route.js
β
βββ services/
β βββ webhook/ β Auth sync service (runs in Docker)
β βββ src/
β βββ index.js β Express entry Β· port 3000 (inside container)
β βββ app.js β CORS Β· middleware setup
β βββ config/
β β βββ firebase-admin.js β Firebase Admin SDK init
β βββ middleware/
β β βββ auth.js β authenticateFirebase β verifies Bearer JWT
β βββ routes/
β β βββ index.js β mounts auth routes Β· /health endpoint
β β βββ auth.js β POST /api/auth/sync-user β upsert public.users
β βββ services/
β βββ db.js β PostgreSQL pool (pg)
β
βββ infra/
β βββ hasura/
β βββ docker-compose.yml β postgres (5433) Β· graphql-engine (8080) Β· webhook
β βββ bin/
β β βββ dc_prep β bootstrap script: up β wait β migrate β seed
β βββ metadata/
β β βββ base/ β shared Hasura config (actions, cron triggersβ¦)
β β βββ build/safetrust/ β generated output β do not edit manually
β β βββ tenants/safetrust/ β source table YAML (permissions Β· relationships)
β βββ migrations/safetrust/ β timestamped SQL migrations
β βββ seeds/safetrust/ β 01_users Β· 02_apartments Β· 03_bid_requests
β
βββ packages/
β βββ graphql/
β β βββ generated/
β β βββ index.ts β codegen output β do not edit (empty until codegen runs)
β βββ types/
β βββ src/
β βββ index.ts β shared TypeScript interfaces
β
βββ pnpm-workspace.yaml
βββ turbo.json
βββ package.json β root Β· scripts: dev Β· build Β· lint
| Package | Name | Port | Status |
|---|---|---|---|
apps/frontend |
@safetrust/web |
3001 |
β
Running β /login and /register compile clean |
apps/api |
@safetrust/api |
3000 |
β Running β nodemon dev server |
services/webhook |
safetrust-webhook |
3000 (container) |
β
Deployed in Docker via infra/hasura/docker-compose.yml |
infra/hasura |
Hasura + PostgreSQL | 8080 / 5433 |
β
Migrations + seeds applied via bin/dc_prep |
packages/types |
@safetrust/types |
β | β Linked via workspace |
packages/graphql |
@safetrust/graphql |
β | pnpm codegen runs |
| Tool | Version | Install |
|---|---|---|
| Docker + Docker Compose | latest | docs.docker.com |
| Node.js | β₯ 18 | nodejs.org |
| pnpm | β₯ 8 | npm install -g pnpm |
| Hasura CLI | latest | npm install -g hasura-cli |
git clone https://github.com/safetrustcr/dApp-SafeTrust.git
cd dApp-SafeTrustAlways run from the repo root. Never run npm install or pnpm install from inside a subdirectory β the workspace:* protocol in package.json files will fail.
pnpm install# Firebase client SDK (Firebase Console β Project Settings β General)
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=
# Backend webhook URL (where the frontend posts auth sync)
NEXT_PUBLIC_BACKEND_URL=http://localhost:3000
# Hasura GraphQL
NEXT_PUBLIC_HASURA_GRAPHQL_URL=http://localhost:8080/v1/graphql
NEXT_PUBLIC_HASURA_WS_URL=ws://localhost:8080/v1/graphqlPOSTGRES_PASSWORD=postgrespassword
HASURA_GRAPHQL_ADMIN_SECRET=myadminsecretkey
HASURA_GRAPHQL_JWT_SECRET='{"type":"RS256","jwk_url":"https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com","audience":"<YOUR_FIREBASE_PROJECT_ID>","issuer":"https://securetoken.google.com/<YOUR_FIREBASE_PROJECT_ID>"}'
# Firebase Admin SDK (Firebase Console β Project Settings β Service Accounts)
FIREBASE_PROJECT_ID=
FIREBASE_CLIENT_EMAIL= # must be the service account email, not a personal Gmail
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
# Webhook shared secret
HASURA_EVENT_SECRET=dev-event-secret-local
WEBHOOK_URL=http://safetrust-webhook:3000cd infra/hasura
bin/dc_prepdc_prep runs in order: starts containers β waits for Hasura healthz β registers the database source β applies migrations β reloads metadata β applies seeds. Takes ~30 seconds on first run.
Verify:
# Hasura console
open http://localhost:8080/console
# Check seed data
docker exec hasura-postgres-1 psql -U postgres -d postgres \
-c "SELECT id, email FROM public.users;"
# Check webhook is alive
curl http://localhost:3000/healthIn a separate terminal, from the repo root:
pnpm run devTurborepo starts apps/frontend and apps/api in parallel.
| URL | Expected |
|---|---|
http://localhost:3001 |
Redirects to /login |
http://localhost:3001/login |
Login form with wallet options |
http://localhost:3001/register |
Register form β Full Name, Phone, Location, Email, Password |
http://localhost:8080/console |
Hasura console |
http://localhost:3000/health |
{ "status": "ok" } |
pnpm --filter frontend run codegenThis introspects the Hasura schema and writes typed Apollo hooks to packages/graphql/generated/index.ts. The file is empty until this runs β it does not block auth flow but is required for escrow queries.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/health |
β | Liveness check |
POST |
/api/auth/sync-user |
Bearer <Firebase JWT> |
Upserts user into public.users after registration |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/health |
β | Liveness check |
POST |
/api/escrow/deploy |
Bearer <Firebase JWT> |
Submit signed XDR to TrustlessWork |
| Table | Purpose |
|---|---|
public.users |
Authenticated users β synced from Firebase on register |
public.user_wallets |
Stellar wallet addresses per user |
public.apartments |
Property listings |
public.bid_requests |
Tenant rental offers (PENDING / CANCELLED / ACTIVE) |
public.trustless_work_escrows |
On-chain escrow mirror |
public.escrow_milestones |
Release schedule per escrow |
public.escrow_transactions |
SafeTrust business transaction log |
public.trustless_work_webhook_events |
Inbound events from TrustlessWork |
public.escrows |
Single-release security deposit escrows |
- 2 demo users (
demo-tenant-uid-001,demo-owner-uid-002) - 2 demo apartments linked to the demo users
- 2 demo bid requests (
PENDING+CANCELLED)
cd infra/hasura
docker compose down -v # removes volumes
bin/dc_prep # fresh start β migrations + seeds reappliedThis section documents the current implementation state so contributors don't spend time debugging already-known gaps.
| Feature | Details |
|---|---|
| Monorepo bootstrap | pnpm install from root resolves all workspace:* deps |
pnpm run dev |
Starts both apps/frontend (port 3001) and apps/api (port 3000) via Turborepo |
| Backend Docker stack | bin/dc_prep boots postgres + Hasura + webhook, applies migrations and seeds |
/login page |
Renders β Firebase auth not yet connected to form submit |
/register page |
Renders β Full Name, Phone (+506 default), Location, Email, Password fields |
| Apollo Client | authLink reads Firebase JWT from auth.currentUser.getIdToken() β wired in config/apollo.ts |
services/webhook |
POST /api/auth/sync-user upserts into public.users with COALESCE for optional fields |
| Hasura metadata | All tables tracked, tenant + landlord roles with row-level permissions |
cn() utility |
Lives at @/lib/utils β all shadcn/ui components import from here |
| Provider tree | layout.tsx β ClientProviders β ApolloProviderWrapper β TrustlessWorkProvider |
| File | Status | What's needed |
|---|---|---|
src/lib/walletconnect.ts |
Stub β returns "" |
Full WalletConnect SignClient init with project ID |
packages/graphql/generated/index.ts |
Empty | Run pnpm --filter frontend run codegen with Hasura live |
src/app/dashboard/page.tsx |
Shell | Wire useQuery(GET_USER) from src/graphql/queries/user-queries.ts |
src/components/auth/Login.tsx |
UI renders | Connect signInWithEmailAndPassword β setToken() β redirect /dashboard |
src/components/auth/Register.tsx |
UI renders | createUserWithEmailAndPassword β getIdToken β POST /api/auth/sync-user β logic exists but needs .env.local to run |
| Feature | Notes |
|---|---|
| Freighter wallet integration | useWalletDetection hook exists, actual Freighter connect not wired |
| Escrow deploy flow | apps/api/src/routes/escrow/deploy.handler.js exists, frontend PAY button not connected |
| GraphQL subscriptions | subscription-client.ts exists, no active subscriptions in UI |
| Email verification flow | VerifyEmail.tsx component exists, Firebase email verification not triggered |
Never use sudo for dev commands. Running pnpm install as root causes node_modules to be owned by root, breaking subsequent runs as your normal user. If you already did this:
sudo chown -R $USER:$USER node_modulesAlways run pnpm commands from the repo root, not from inside apps/frontend/ or any subdirectory. The workspace:* protocol only resolves from the workspace root.
pnpm --filter frontend dev uses the package name β the filter value must match the name field in package.json. For this repo that name is @safetrust/web, so use:
pnpm run dev # starts everything (recommended)
# or target a single app:
pnpm --filter @safetrust/web dev
pnpm --filter @safetrust/api devThe node_modules root is owned-by-root if you ran pnpm install with sudo su at any point. Fix with sudo chown -R $USER:$USER node_modules from the repo root before continuing.
Firebase FIREBASE_CLIENT_EMAIL must be the service account email (the one ending in iam.gserviceaccount.com), not a personal Gmail address. Using a personal email will cause firebase-admin to throw on startup.
FIREBASE_PRIVATE_KEY newlines β the private key must have literal \n in the .env file. If you copy from the Firebase console JSON, replace actual newlines with \n or wrap the value in double quotes with escaped newlines.
pnpm run devpnpm --filter @safetrust/web dev
pnpm --filter @safetrust/api devpnpm buildTurborepo builds packages/types β packages/graphql β then apps.
pnpm lint- Read this README fully, especially the Known State section
- Check existing issues β most missing pieces are already tracked
- Run the stack locally end-to-end before opening a PR
If your issue has an unresolved import from a package another contributor is building in parallel, stub it:
// TODO: wire in Batch 2 β @/core/store/data
const useGlobalAuthenticationStore = () => ({ address: null, setToken: () => {} });This keeps the build green while the dependency is in flight.
-
pnpm run devstarts without errors -
/loginand/registerstill compile (check browser console too) - No new
// @ts-ignoreoranywithout a comment explaining why - No
console.logleft in production paths - Include a short description of what was wired and what remains stubbed
- Link the issue your PR closes
feat/<issue-number>-short-description
fix/<issue-number>-short-description
| Repository | Purpose |
|---|---|
| frontend-SafeTrust | Full Next.js frontend (source for apps/frontend slices) |
| backend-SafeTrust | Full Hasura GraphQL backend (source for metadata/migrations/seeds slices) |
| landing-SafeTrust | Marketing landing page |
- TrustlessWork API Docs
- Hasura GraphQL Docs
- Stellar Developer Docs
- Freighter Wallet API
- Firebase Auth Docs
- pnpm Workspaces
- Turborepo Docs
Built with π by the SafeTrust team Β· safetrustcr.vercel.app