# Project export: Vowch

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Vowch, link up, get it done
- Devpost: https://devpost.com/software/vowch
- GitHub: https://github.com/AkilSaji/vowch
- Demo: https://vowch-marketing.vercel.app/
- Video: https://www.youtube.com/embed/Qawi9_3t5k4?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Akil Saji (66 commits)

## Devpost submission (written by the team)

### Inspiration

ok, so the inspiration came from my life, itself. So my name is Akil Saji, as of right now i am working, in the intersection between, ai, marketing and development. So basically i am from a middle class background, so when i first came to the city Banglore, it was hard finding the job, India as you know, is a country with high population, and high unemployment rates, so it was hard to finding a job, even after getting a Engineering degree, so then did many blue collar jobs, like, washing dishes at Taco Bell, driving Uber, food delivery(Zomato), i mean, my thing was to, go explore life, but learnt a lot about life, too. That is the space where i discovered gig work, because, if i am facing this problem, then sure as hell, many students, many people, no matter the age, face these problems too, and yes, gig work was on rise too at that time, so that is when i decided, if one day, i get to a point where, i can do something, to solve this problem, i will... so hustled hard, learnt all, i can, in development, ai, marketing etc. So yes, basically you can say, this is my inspiration

### What it does

So my app is called Vowch, where it is marketplace for gig works, but with referral, so yes, it is liek many other platforms, but here, the person, who can do the gig work has to be vouched by someone, hence, filtering the right set of people, so basically, it its more aligned towards, lets any, skilled gig work, but with a network. each and everyone in the network, is vouched by someone, already in the gig network, hence maintaining the quality and trust. So i have built a client portal as an web app, where the the person, you want the job done can post a gig work, called poster, and the person who doe sth work can login, into , and also, there is an app too, as app cannot be hosted, i have pushed the code for it. So, basically, if anyone has to join, they can scan. qr or should be referred by someone, so as a builder, i have a skill passport, where each an everyone is verified though. Skill passport, so that is the skill, and each and everyone has a skill passport number which is unique. So after sign in, anyone can pst a gig, but if a perosn has to do a gig, then he has to be vouched by someone, and only with the skill passport, that guy will be able to complete, a gig, and qr code verification is there, for everything... for verifying the network, and to identify, the correct, gig person has reached the poster, and thing like that. and if that voucher behaves inappropriately, then the Cred will go down. So, in the beginning, each and everyone will get a Cred of 500, then everything depends upon the cred and the work they do

### How we built it

So, i used Codex to built, with mostly with the Codex 5.6 Terra model, the bakcend is in AWS, hosted in Vercel, and pushed though Githb for version control, then used Expo for locally hosting the app and testing,a dn also used Mapbox, for getting the map coordinates for the gig work. I had some sort of architecture in my mind, but making that into a system and product was the hardest the web app.... screen flow and everything And integrating the autonomous AI agent Sentinel, run by Llama 3.3 70B model for taking actions

### Challenges we ran into

Payment and escrow, was the hardest and verification on how we can verify the gig worker, is genuine as he say he is or vouched by the guy. So it is the first time i am building an app, so the app screen flow, , it was entirely new to me, couldn't figure out half the things, but still with the help of Codex and MCP, which i connected taught me a lot and helped me

### Accomplishments we're proud of

Building the entire backend, two web portal, and an app, obviously and, turning our idea into an MVP

### What we learned

Verification and payment still is an edge case, it will take a lot of fine-tuning an refinement, and the vouching part which is the main feature of the product is still unexplored, if this app get traction, then we may have to do something with the referral part, because, that is like an blocker, and a filter system i have used to block out the unqualified people, and frauds.

### What's next

Scaling up.............. building an elite community, for skilled market professional, who will get gig through vowch.

## README (from the GitHub repository)

# Vowch — trusted gig-work marketplace

Vowch is a marketplace for skilled gig work built around trust. Clients can post
gigs, and workers use a Skill Passport, referrals ("vouches"), reputation, and
QR verification to help make sure the right person completes the work.

This repository contains the Vowch web portals, mobile app, serverless backend,
infrastructure, and deployment workflow.

## Project structure

- `apps/client-portal/` — client-facing web portal for posting and managing gigs
- `apps/admin-portal/` — administrator portal
- `apps/mobile/` — Expo / React Native mobile app for workers
- `backend/` — AWS SAM serverless API and worker services
- `infra/` — CloudFormation bootstrap and security stacks

## Built with Codex and GPT-5.6

Vowch was built during OpenAI Build Week with Codex using GPT-5.6. Codex was used
as a development partner to turn the product architecture into working web,
mobile, backend, and infrastructure code; iterate on user flows and interface
details; help integrate AWS, Cognito, Mapbox, Expo, and Vercel; and validate and
refine the implementation. The product decisions, testing, and final integration
were directed by the builder.

## Backend and infrastructure

The serverless backend is implemented with AWS SAM application code in
`backend/`, CloudFormation bootstrap and security stacks in `infra/`, and the
GitHub Actions deployment workflow in `.github/workflows/`.

The stack uses API Gateway, Lambda, DynamoDB, Cognito, SQS, EventBridge,
WebSockets, and a private S3 upload bucket. GitHub Actions authenticates to AWS
through OIDC; no AWS access key or secret key is stored in GitHub.

## Deployment

Before the first GitHub Actions deployment, deploy `infra/bootstrap.yaml` once
with an administrator AWS session and copy the resulting `GitHubDeployRoleArn`
CloudFormation output into the `AWS_DEPLOY_ROLE_ARN` GitHub Actions secret.
Then configure the repository secrets and variables listed in
`backend/deployment.md` and push to `main`.

For local validation:

```bash
npm ci --prefix backend
npm test --prefix backend
npm run lint --prefix backend
sam validate --template-file backend/template.yaml
```


## Detected evidence (automated analysis)

Indexed codebase: 71 recognized source files, 1636 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- AWS (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (120 of 134)

```
.github/workflows/deploy-full-vowch.yml
.gitignore
apps/admin-portal/.env.example
apps/admin-portal/index.html
apps/admin-portal/package.json
apps/admin-portal/README.md
apps/admin-portal/src/cognito.ts
apps/admin-portal/src/main.tsx
apps/admin-portal/src/reference.css
apps/admin-portal/src/styles.css
apps/admin-portal/src/vite-env.d.ts
apps/admin-portal/tsconfig.json
apps/admin-portal/tsconfig.node.json
apps/admin-portal/vercel.json
apps/admin-portal/vite.config.ts
apps/client-portal/.env.example
apps/client-portal/index.html
apps/client-portal/package.json
apps/client-portal/README.md
apps/client-portal/src/api.ts
apps/client-portal/src/cognito.ts
apps/client-portal/src/main.tsx
apps/client-portal/src/styles.css
apps/client-portal/src/vite-env.d.ts
apps/client-portal/tsconfig.json
apps/client-portal/tsconfig.node.json
apps/client-portal/vercel.json
apps/client-portal/vite.config.ts
apps/mobile/.env.example
apps/mobile/app.json
apps/mobile/App.tsx
apps/mobile/package.json
apps/mobile/README.md
apps/mobile/src/api.ts
apps/mobile/src/cognito.ts
apps/mobile/src/data.ts
apps/mobile/src/location-map-preview.native.tsx
apps/mobile/src/location-map-preview.tsx
apps/mobile/src/location-map-preview.web.tsx
apps/mobile/src/onboarding.tsx
apps/mobile/src/theme.ts
apps/mobile/src/types.ts
apps/mobile/tsconfig.json
backend/deployment.md
backend/docs/admin-deployment.md
backend/openapi.yaml
backend/package.json
backend/README.md
backend/samconfig.toml
backend/scripts/backfill-gig-indexes.mjs
backend/scripts/backfill-worker-indexes.mjs
backend/scripts/migrate.mjs
backend/scripts/seed-founder.mjs
backend/src/admin/audit.mjs
backend/src/app.mjs
backend/src/auth-triggers.mjs
backend/src/auth/permissions.mjs
backend/src/auto-release.mjs
backend/src/email.mjs
backend/src/gig-expiry.mjs
backend/src/malware-scan.mjs
backend/src/notification-outbox.mjs
backend/src/notification-retry.mjs
backend/src/payments.mjs
backend/src/rating-reveal.mjs
backend/src/realtime.mjs
backend/src/reconcile.mjs
backend/src/trust-sentinel.mjs
backend/src/worker.mjs
backend/template.yaml
backend/tests/admin-api.integration.test.mjs
backend/tests/admin-permissions.test.mjs
backend/tests/dynamodb.integration.test.mjs
backend/tests/gmail-config.test.mjs
backend/tests/provider.integration.test.mjs
backend/tests/smoke.test.mjs
backend/tests/workflow.integration.test.mjs
DEPLOY_TO_VERCEL.md
infra/bootstrap.yaml
infra/README.md
infra/security.md
infra/security.yaml
README.md
Vowch website/__nuxt_content/calculator_en/sql_dump.txt
Vowch website/__nuxt_content/calculator_fr/sql_dump.txt
Vowch website/__nuxt_content/contact_en/sql_dump.txt
Vowch website/__nuxt_content/contact_fr/sql_dump.txt
Vowch website/__nuxt_content/content_en/sql_dump.txt
Vowch website/__nuxt_content/content_fr/sql_dump.txt
Vowch website/__sitemap__/en-US.xml
Vowch website/_i18n/e3b0c442/en/messages.json
Vowch website/_nuxt/6ON4gZRy.js
Vowch website/_nuxt/B2Phc4Qb.js
Vowch website/_nuxt/B7hRrOYm.js
Vowch website/_nuxt/Bah3IIkX.js
Vowch website/_nuxt/BCardsLoop.dqio52BG.css
Vowch website/_nuxt/BFluid.bxahTSu7.css
Vowch website/_nuxt/Bl1peWe-.js
Vowch website/_nuxt/bsXMvv-l.js
Vowch website/_nuxt/builds/meta/9bdc0ec3-5189-4495-8b2b-16b13c5bcaed.json
Vowch website/_nuxt/builds/meta/b0a1c2d3-1111-4222-8333-444455556666.json
Vowch website/_nuxt/C0_wrGlg.js
Vowch website/_nuxt/C0AaKDGT.js
Vowch website/_nuxt/CQmIGG9a.js
Vowch website/_nuxt/CqR-MeVM.js
Vowch website/_nuxt/DdWAC-bf.js
Vowch website/_nuxt/DMVSqEZI.js
Vowch website/_nuxt/DQy71oTq.js
Vowch website/_nuxt/dXHoGFCM.js
Vowch website/_nuxt/entry.1Cj3YW7s.css
Vowch website/_nuxt/FFX9UyLS.js
Vowch website/_nuxt/index.z_HNF_kd.css
Vowch website/_nuxt/legals.C1dGNBB7.css
Vowch website/_nuxt/privacy.DrkBNHHB.css
Vowch website/_nuxt/sqlite3.DBpDb1lf.wasm
Vowch website/_nuxt/t_T8Sler.js
Vowch website/_payload_9bdc0ec3_5189_4495_8b2b_16b13c5bcaed.json
Vowch website/_payload_b0a1c2d3_1111_4222_8333_444455556666.json
Vowch website/_payload.json
Vowch website/beacon.min.js/v4513226cdae34746b4dedf0b4dfa099e1781791509496
[14 more files omitted for size]
```

### Dependencies

- apps/admin-portal/package.json: @types/node@^26.1.1, @types/react@^18.3.18, @types/react-dom@^18.3.5, @vitejs/plugin-react@^4.3.4, lucide-react@^0.468.0, react@^18.3.1, react-dom@^18.3.1, typescript@^5.7.3, vite@^6.2.0
- apps/client-portal/package.json: @types/node@^26.1.1, @types/react@^18.3.18, @types/react-dom@^18.3.5, @vitejs/plugin-react@^4.3.4, lucide-react@^0.468.0, mapbox-gl@^3.26.0, react@^18.3.1, react-dom@^18.3.1, typescript@^5.7.3, vite@^6.2.0
- apps/mobile/package.json: @expo-google-fonts/baloo-2@^0.4.2, @expo-google-fonts/nunito@^0.4.2, @expo/vector-icons@^15.0.3, @types/react@~19.1.10, expo@^54.0.0, expo-font@~14.0.12, expo-image-picker@~17.0.11, expo-linear-gradient@~15.0.8, expo-location@~19.0.8, expo-status-bar@~3.0.9, lucide-react-native@^1.25.0, qrcode@^1.5.4, react@19.1.0, react-dom@19.1.0, react-native@0.81.5, react-native-maps@1.20.1, react-native-safe-area-context@~5.6.0, react-native-svg@^15.15.5, react-native-web@^0.21.0, typescript@~5.9.2
- backend/package.json: @aws-sdk/client-apigatewaymanagementapi@^3.826.0, @aws-sdk/client-cognito-identity-provider@^3.826.0, @aws-sdk/client-dynamodb@^3.826.0, @aws-sdk/client-eventbridge@^3.826.0, @aws-sdk/client-s3@^3.826.0, @aws-sdk/client-sqs@^3.826.0, @aws-sdk/client-ssm@^3.826.0, @aws-sdk/lib-dynamodb@^3.826.0, @aws-sdk/s3-request-presigner@^3.826.0, aws-jwt-verify@^5.1.1, nodemailer@^9.0.3, pdf-parse@^1.1.1

### Recent commits (newest first)

- Document Vowch hackathon project
- Merge pull request #26 from AkilSaji/codex/remove-agency-copy
- Remove remaining agency copy from marketing site
- Merge pull request #25 from AkilSaji/codex/clean-marketing-template
- Remove agency pricing and enquiry surfaces
- Merge pull request #24 from AkilSaji/codex/restore-mobile-onboarding
- Restore mobile welcome onboarding
- Merge pull request #23 from AkilSaji/codex/refine-mobile-passport
- Refine mobile skill passport layout
- Merge pull request #22 from AkilSaji/codex/mobile-flow-qa
- Fix mobile onboarding and app flow QA
- Merge pull request #21 from AkilSaji/codex/mobile-interactive-skill-passport
- Add interactive skill passport to mobile app
- Merge pull request #20 from AkilSaji/codex/interactive-skill-passport
- Add interactive Vowch skill passport
- Merge pull request #19 from AkilSaji/codex/mobile-portal-ui
- Align mobile UI with client portal
- Merge pull request #18 from AkilSaji/codex/open-explorer-vouch-flow
- Allow open exploration before Vouching
- Merge pull request #17 from AkilSaji/codex/admin-access-denied-screen

## Key source files (fetched from GitHub, selected and truncated for size)

### DEPLOY_TO_VERCEL.md

```markdown
# Vowch hackathon deployment

Use Vercel for three independently hosted sites from this one repository. A paid
domain is not needed: each receives a public `*.vercel.app` address.

## 1. Marketing website

- Create project: `vowch-marketing`
- Root Directory: `Vowch website`
- Framework Preset: `Other`
- Build Command: leave blank
- Output Directory: leave blank

The folder is already a static website and Vercel will serve its `index.html`.

## 2. Client portal

- Create project: `vowch-client`
- Root Directory: `apps/client-portal`
- Framework Preset: `Vite`
- Build Command: `npm run build`
- Output Directory: `dist`

## 3. Admin portal

- Create project: `vowch-admin`
- Root Directory: `apps/admin-portal`
- Framework Preset: `Vite`
- Build Command: `npm run build`
- Output Directory: `dist`
- Environment Variable: `VITE_API_URL` = the API Gateway `ApiUrl` output from the
  deployed Vowch AWS SAM stack.

After Vercel deploys the admin portal, set the backend `PUBLIC_APP_ORIGIN` value to
the exact admin Vercel URL and redeploy the backend. This enables browser CORS
requests. The client portal currently runs its supplied hackathon demo data, so it
does not need an API environment variable to be judged.

## Submission links

Submit the three Vercel URLs, the Expo Go mobile QR/APK, the GitHub repository URL,
and a short demo video. Label demo data clearly where it is used.

```

### infra/security.md

```markdown
# Secure upload infrastructure

Deploy `security.yaml` after the bootstrap stack. It creates the quarantine bucket, malware scan queues, clean-file queue, proof-review queue, dead-letter queues, security SNS topic, and scan alarm.

Malware scanning is intentionally deferred. The upload bucket remains private and encrypted; ClamAV can be added later as a separate production hardening stack.

```bash
sam deploy --template-file infra/security.yaml --stack-name vowch-security-dev --capabilities CAPABILITY_IAM
```

```

### backend/package.json

```
{
  "name": "vowch-backend",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --test",
    "test:integration": "node --test tests/*.integration.test.mjs",
    "test:admin:integration": "node --test tests/admin-api.integration.test.mjs",
    "lint": "node --check src/app.mjs && node --check src/auth-triggers.mjs && node --check src/email.mjs && node --check src/notification-outbox.mjs && node --check src/notification-retry.mjs && node --check src/worker.mjs && node --check src/auth/permissions.mjs && node --check src/admin/audit.mjs",
    "seed:founder": "node scripts/seed-founder.mjs",
    "migrate": "node scripts/migrate.mjs",
    "migrate:gigs": "node scripts/backfill-gig-indexes.mjs",
    "migrate:worker-indexes": "node scripts/backfill-worker-indexes.mjs"
  },
  "dependencies": {
    "@aws-sdk/client-apigatewaymanagementapi": "^3.826.0",
    "@aws-sdk/client-cognito-identity-provider": "^3.826.0",
    "@aws-sdk/client-dynamodb": "^3.826.0",
    "@aws-sdk/client-eventbridge": "^3.826.0",
    "@aws-sdk/client-s3": "^3.826.0",
    "@aws-sdk/client-sqs": "^3.826.0",
    "@aws-sdk/client-ssm": "^3.826.0",
    "@aws-sdk/lib-dynamodb": "^3.826.0",
    "@aws-sdk/s3-request-presigner": "^3.826.0",
    "aws-jwt-verify": "^5.1.1",
    "nodemailer": "^9.0.3",
    "pdf-parse": "^1.1.1"
  }
}

```

### apps/admin-portal/package.json

```
{
  "name": "vowch-admin-portal",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@vitejs/plugin-react": "^4.3.4",
    "lucide-react": "^0.468.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "typescript": "^5.7.3",
    "vite": "^6.2.0"
  },
  "devDependencies": {
    "@types/node": "^26.1.1",
    "@types/react": "^18.3.18",
    "@types/react-dom": "^18.3.5"
  }
}

```

### apps/client-portal/package.json

```
{
  "name": "vowch-client-portal",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@vitejs/plugin-react": "^4.3.4",
    "lucide-react": "^0.468.0",
    "mapbox-gl": "^3.26.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "typescript": "^5.7.3",
    "vite": "^6.2.0"
  },
  "devDependencies": {
    "@types/node": "^26.1.1",
    "@types/react": "^18.3.18",
    "@types/react-dom": "^18.3.5"
  }
}

```

### apps/mobile/package.json

```
{
  "name": "vowch-mobile",
  "version": "0.1.0",
  "private": true,
  "main": "expo/AppEntry",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "lint": "tsc --noEmit"
  },
  "dependencies": {
    "@expo-google-fonts/baloo-2": "^0.4.2",
    "@expo-google-fonts/nunito": "^0.4.2",
    "@expo/vector-icons": "^15.0.3",
    "@types/react": "~19.1.10",
    "expo": "^54.0.0",
    "expo-font": "~14.0.12",
    "expo-image-picker": "~17.0.11",
    "expo-linear-gradient": "~15.0.8",
    "expo-location": "~19.0.8",
    "expo-status-bar": "~3.0.9",
    "lucide-react-native": "^1.25.0",
    "qrcode": "^1.5.4",
    "react": "19.1.0",
    "react-dom": "19.1.0",
    "react-native": "0.81.5",
    "react-native-maps": "1.20.1",
    "react-native-safe-area-context": "~5.6.0",
    "react-native-svg": "^15.15.5",
    "react-native-web": "^0.21.0",
    "typescript": "~5.9.2"
  }
}

```

### apps/admin-portal/src/main.tsx

```typescript
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Activity, AlertTriangle, BadgeCheck, Bell, BriefcaseBusiness, ChevronRight, CircleDollarSign, ClipboardCheck, FileCheck2, Landmark, LogOut, MoreVertical, RefreshCw, Search, ShieldAlert, ShieldCheck, Sparkles, UserRoundCog, Users } from 'lucide-react';
import { createRoot } from 'react-dom/client';
import './styles.css';
import './reference.css';
import { adminAuth } from './cognito';

type RecordItem = Record<string, unknown>;
type Page = 'overview' | 'identity' | 'skills' | 'proof' | 'trust' | 'disputes' | 'users' | 'gigs' | 'payments' | 'orphans' | 'audit' | 'delivery' | 'access';
type ApiPage = { items?: RecordItem[]; nextToken?: string | null; [key: string]: unknown };
type Action = { label: string; tone?: 'primary' | 'danger' | 'quiet'; run: () => Promise<void> };

const API_URL = (import.meta.env.VITE_API_URL || '').replace(/\/$/, '');
const pageMeta: { id: Page; label: string; icon: typeof Activity; endpoint?: string; permission: string }[] = [
  { id: 'overview', label: 'Overview', icon: Activity, permission: 'All operations' },
  { id: 'identity', label: 'Identity reviews', icon: BadgeCheck, endpoint: '/v1/admin/identity-reviews', permission: 'Trust' },
  { id: 'skills', label: 'Skill reviews', icon: ClipboardCheck, endpoint: '/v1/admin/skill-reviews', permission: 'Trust' },
  { id: 'proof', label: 'Proof review', icon: FileCheck2, endpoint: '/v1/admin/proof-review-queue', permission: 'Operations' },
  { id: 'trust', label: 'Trust cases', icon: ShieldCheck, endpoint: '/v1/admin/trust-cases', permission: 'Trust' },
  { id: 'disputes', label: 'Disputes', icon: AlertTriangle, endpoint: '/v1/admin/disputes', permission: 'Finance' },
  { id: 'users', label: 'Members', icon: Users, endpoint: '/v1/admin/users', permission: 'Support' },
  { id: 'gigs', label: 'Gigs', icon: BriefcaseBusiness, endpoint: '/v1/admin/gigs', permission: 'Operations' },
  { id: 'payments', label: 'Payments', icon: CircleDollarSign, endpoint: '/v1/admin/payments', permission: 'Finance' },
  { id: 'orphans', label: 'Lineage review', icon: ShieldAlert, endpoint: '/v1/admin/orphans', permission: 'Trust' },
  { id: 'audit', label: 'Audit log', icon: Landmark, endpoint: '/v1/admin/audit-logs', permission: 'Read only' },
  { id: 'delivery', label: 'Delivery health', icon: Bell, endpoint: '/v1/admin/notification-delivery', permission: 'Read only' },
  { id: 'access', label: 'Admin access', icon: UserRoundCog, endpoint: '/v1/admin/access', permission: 'Super admin only' },
];
const hiddenKeys = new Set(['pk', 'sk', 'gsi1pk', 'gsi1sk', 'gsi2pk', 'gsi2sk', 'gsi3pk', 'gsi3sk', 'gsi4pk', 'gsi4sk', 'nextToken']);
const getItems = (data: unknown): RecordItem[] => Array.isArray(data) ? data as RecordItem[] : data && typeof data === 'object' && Array.isArray((data as ApiPage).items) ? (data as ApiPage).items || [] : [];
const text = (value: unknown) => value === null || value === undefined || value === '' ? '—' : typeof value === 'object' ? JSON.stringify(value) : String(value);
const formatTime = (value: unknown) => { if (!value) return '—'; const date = new Date(String(value)); return Number.isNaN(date.valueOf()) ? text(value) : date.toLocaleString(); };
const label = (value: string) => value.replace(/([A-Z])/g, ' $1').replace(/_/g, ' ').replace(/^./, (letter) => letter.toUpperCase());

function App() {
  const [token, setToken] = useState(''); const [email, setEmail] = useState(''); const [code, setCode] = useState(''); const [challenge, setChallenge] = useState(''); const [authBusy, setAuthBusy] = useState(false);
  const [page, setPage] = useState<Page>('overview'); const [items, setItems] = useState<RecordItem[]>([]); const [nextToken, setNextToken] = useState<string | null>(null); const [summary, setSummary] = useState<RecordItem | null>(null); const [activity, setActivity] = useState<RecordItem[]>([]);
  const [search, setSearch] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [notice, setNotice] = useState(''); const [lastUpdated, setLastUpdated] = useState<Date | null>(null); const [detail, setDetail] = useState<RecordItem | null>(null); const [actionBusy, setActionBusy] = useState(''); const [accessDenied, setAccessDenied] = useState(false);

  const request = useCallback(async (path: string, init: RequestInit = {}) => {
    if (!API_URL) throw new Error('VITE_API_URL is not configured in Vercel.');
    const response = await fetch(`${API_URL}${path}`, { ...init, headers: { Accept: 'application/json', Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...init.headers } });
    const body = await response.json().catch(() => ({}));
    if (!response.ok) throw new Error((body as Record<string, string>).code || (body as Record<string, string>).message || `Request failed (${response.status})`);
    return body as ApiPage;
  }, [token]);

  const load = useCallback(async (cursor?: string | null, append = false) => {
    if (!token) return; setLoading(true); setError(''); setNotice('');
    try {
      if (page === 'overview') {
        const [summaryData, activityData] = await Promise.all([request('/v1/admin/dashboard/summary'), request('/v1/admin/dashboard/activity?limit=12')]);
        setSummary(summaryData); setActivity(getItems(activityData)); setItems([]); setNextToken(null);
      } else {
        const selected = pageMeta.find((item) => item.id === page)!;
        const params = new URLSearchParams({ limit: '50' });
        if (cursor) params.set('nextToken', cursor);
        if (page === 'users' && search.trim()) params.set('query', search.trim());
        const data = await request(`${selected.endpoint}?${params.toString()}`);
        const records = getItems(data); setItems((current) => append ? [...current, ...records] : records); setNextToken(typeof data.nextToken === 'string' ? data.nextToken : null);
      }
      setLastUpdated(new Date());
    } catch 
[truncated — 21809 more characters]
```

### apps/client-portal/src/main.tsx

```typescript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import { Bell, BriefcaseBusiness, ChevronRight, CircleDollarSign, Compass, House, LogOut, MapPin, Menu, MessageCircle, Plus, Search, Send, ShieldCheck, Sparkles, UserRound, WalletCards, X } from 'lucide-react';
import { createRoot } from 'react-dom/client';
import { createApi, type ApiGig, type Profile } from './api';
import { cognitoAuth } from './cognito';
import './styles.css';

type Tab = 'discover' | 'post' | 'jobs' | 'messages' | 'passport' | 'house' | 'wallet' | 'settings';
type Session = { accessToken: string; email: string };
type Workspace = { profile: Profile | null; gigs: ApiGig[]; applications: Record<string, unknown>[]; workerGigs: ApiGig[]; earnings: Record<string, unknown>; poster: Record<string, unknown>; passports: Record<string, unknown>[]; notifications: Record<string, unknown>[]; communities: Record<string, unknown>[]; house: Record<string, unknown> };
const nav: { id: Tab; label: string; icon: typeof Compass }[] = [
  { id: 'discover', label: 'Discover', icon: Compass }, { id: 'post', label: 'Post a gig', icon: Plus }, { id: 'jobs', label: 'My jobs', icon: BriefcaseBusiness }, { id: 'messages', label: 'Messages', icon: MessageCircle }, { id: 'passport', label: 'My passport', icon: ShieldCheck }, { id: 'house', label: 'My House', icon: House }, { id: 'wallet', label: 'Wallet', icon: WalletCards }, { id: 'settings', label: 'Settings', icon: UserRound },
];
const emptyWorkspace: Workspace = { profile: null, gigs: [], applications: [], workerGigs: [], earnings: {}, poster: {}, passports: [], notifications: [], communities: [], house: {} };
const money = (value: unknown) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format((Number(value) || 0) / 100);
const label = (value: unknown) => String(value || '').replace(/([A-Z])/g, ' $1').replace(/_/g, ' ');
const initials = (name?: string) => (name || 'Vowch').split(' ').map((word) => word[0]).join('').slice(0, 2).toUpperCase();
const mapboxToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN || '';
const bengaluruPlaces = [
  { name: 'Indiranagar', coordinates: [77.6412, 12.9784] as [number, number] },
  { name: 'Koramangala', coordinates: [77.6245, 12.9352] as [number, number] },
  { name: 'HSR Layout', coordinates: [77.6387, 12.9116] as [number, number] },
  { name: 'Whitefield', coordinates: [77.7500, 12.9698] as [number, number] },
];

function App() {
  const [session, setSession] = useState<Session | null>(() => { try { return JSON.parse(sessionStorage.getItem('vowch-session') || 'null'); } catch { return null; } });
  if (!session) return <Auth onAuthenticated={(next) => { sessionStorage.setItem('vowch-session', JSON.stringify(next)); setSession(next); }} />;
  return <WorkspaceApp session={session} signOut={() => { sessionStorage.removeItem('vowch-session'); setSession(null); }} />;
}

function Auth({ onAuthenticated }: { onAuthenticated: (session: Session) => void }) {
  const [stage, setStage] = useState<'email' | 'confirm' | 'otp'>('email'); const [email, setEmail] = useState(''); const [code, setCode] = useState(''); const [challenge, setChallenge] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState('');
  const start = async () => { setBusy(true); setError(''); try { setChallenge(await cognitoAuth.startOtp(email.trim().toLowerCase())); setStage('otp'); } catch (caught) { setError(caught instanceof Error ? caught.message : 'Unable to start sign-in.'); } finally { setBusy(false); } };
  const verify = async () => { setBusy(true); setError(''); try { const result = await cognitoAuth.answerOtp(email.trim().toLowerCase(), challenge, code); onAuthenticated({ accessToken: result.accessToken, email }); } catch (caught) { setError(caught instanceof Error ? caught.message : 'That code did not work.'); } finally { setBusy(false); } };
  const action = stage === 'email' ? start : verify;
  return <main className="auth-page"><section className="auth-art"><div className="brand"><i>v</i> vowch</div><p className="eyebrow">TRUSTED WORK, HUMANLY VERIFIED</p><h1>A better way to get things <em>done.</em></h1><p>Browse work, build your trust passport, and exchange value with people who can vouch for one another.</p><div className="auth-orbit"><span>trust</span><span>work</span><span>community</span></div></section><section className="auth-form"><div><p className="eyebrow">WELCOME TO VOWCH</p><h2>{stage === 'email' ? 'Start with your email' : stage === 'confirm' ? 'Confirm your email' : 'Check your inbox'}</h2><p className="muted">{stage === 'email' ? 'We use a short email code instead of passwords.' : stage === 'confirm' ? `Enter the confirmation code sent to ${email}.` : `Enter the 6-digit sign-in code sent to ${email}.`}</p></div><label>Email address<input disabled={stage !== 'email'} type="email" value={email} onChange={(event) => setEmail(event.target.value)} placeholder="you@example.com" autoComplete="email" /></label>{stage !== 'email' && <label>Code<input inputMode="numeric" autoComplete="one-time-code" maxLength={6} value={code} onChange={(event) => setCode(event.target.value.replace(/\D/g, ''))} placeholder="123456" autoFocus /></label>}<button className="primary" disabled={busy || !email || (stage !== 'email' && code.length !== 6)} onClick={() => void action()}>{busy ? 'Please wait…' : stage === 'email' ? 'Continue with email' : stage === 'confirm' ? 'Confirm email' : 'Sign in securely'} <ChevronRight size={17} /></button>{stage === 'otp' && <button className="text-button" disabled={busy} onClick={() => void (async () => { try { setBusy(true); const otp = await cognitoAuth.beginOtp(email); setChallenge(otp); setCode(''); } catch (caught) { setError(caught instanceof Error ? caught.message : 'Could not resend code.'); } finally { setBusy(false); } })()}>Send another code</button>}{stage !== 'email' && <button classNam
[truncated — 25762 more characters]
```

### Vowch website/nocache_server.py

```python
import sys
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer

class H(SimpleHTTPRequestHandler):
    def log_message(self, format, *args):
        # This server is launched as a background local preview, so it has no
        # interactive console available for request logging.
        pass

    def end_headers(self):
        self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
        self.send_header('Pragma', 'no-cache')
        self.send_header('Expires', '0')
        super().end_headers()

if __name__ == '__main__':
    port = int(sys.argv[1]) if len(sys.argv) > 1 else 8935
    directory = sys.argv[2] if len(sys.argv) > 2 else '.'
    import functools
    handler = functools.partial(H, directory=directory)
    ThreadingHTTPServer(('127.0.0.1', port), handler).serve_forever()

```

### infra/security.yaml

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'Vowch secure upload and proof-review infrastructure'

Parameters:
  Environment: { Type: String, Default: dev }

Resources:
  UploadBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      BucketName: !Sub 'vowch-uploads-${AWS::AccountId}-${Environment}'
      BucketEncryption: { ServerSideEncryptionConfiguration: [{ ServerSideEncryptionByDefault: { SSEAlgorithm: AES256 } }] }
      PublicAccessBlockConfiguration: { BlockPublicAcls: true, BlockPublicPolicy: true, IgnorePublicAcls: true, RestrictPublicBuckets: true }
      VersioningConfiguration: { Status: Enabled }
      LifecycleConfiguration:
        Rules: [{ Id: AbortMultipartUploads, Status: Enabled, AbortIncompleteMultipartUpload: { DaysAfterInitiation: 2 } }]

  UploadBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref UploadBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement: [{ Effect: Deny, Principal: '*', Action: 's3:*', Resource: [!GetAtt UploadBucket.Arn, !Sub '${UploadBucket.Arn}/*'], Condition: { Bool: { 'aws:SecureTransport': false } } }]


  ProofReviewDeadLetterQueue:
    Type: AWS::SQS::Queue
    Properties: { MessageRetentionPeriod: 1209600 }
  ProofReviewQueue:
    Type: AWS::SQS::Queue
    Properties:
      VisibilityTimeout: 120
      RedrivePolicy: { deadLetterTargetArn: !GetAtt ProofReviewDeadLetterQueue.Arn, maxReceiveCount: 3 }

  SecurityAlerts:
    Type: AWS::SNS::Topic
    Properties: { TopicName: !Sub 'vowch-security-${Environment}' }

Outputs:
  UploadBucket: { Value: !Ref UploadBucket }
  ProofReviewQueueUrl: { Value: !Ref ProofReviewQueue }
  SecurityAlertsTopicArn: { Value: !Ref SecurityAlerts }

```

[53 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]