Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- run: bun run lint
- run: bun run typecheck
- run: bun run audit:high
- run: bun x expo-doctor
- run: bun run doctor
- run: bun run test:worker-security
- run: bun run worker:check
- run: bun x expo export --platform web
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
# dependencies
node_modules/

# Bun is the only supported package manager. Ignoring the npm and yarn lock
# files keeps a stray `npm install` from committing a second lockfile, which
# breaks package manager inference on EAS Build.
package-lock.json
yarn.lock

# Expo
.expo/
dist/
Expand Down
65 changes: 33 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,17 @@ ListingOS is a working MVP, not a finished multi-marketplace production platform
### Prerequisites

- Node.js compatible with Expo SDK 57
- Bun 1.3 or newer, which manages dependencies and runs the package scripts
- Android Studio and the Android SDK for local Android builds
- A Cloudflare account with Workers, D1, KV, R2, and Queues
- OpenAI and eBay sandbox or production credentials
- Wrangler authenticated with `npx wrangler login`
- Wrangler authenticated with `bun x wrangler login`

### Install and validate

```bash
npm install
npm run check
bun install
bun run check
```

### Run the mobile app
Expand All @@ -217,19 +218,19 @@ The default client configuration points to the deployed Worker defined in `src/c
Start the Expo development-client server:

```bash
npm run dev
bun run dev
```

Use tunnel mode when the test device cannot reach the development server over the local network:

```bash
npm run dev:tunnel
bun run dev:tunnel
```

For a direct native Android build:

```bash
npm run android
bun run android
```

An optional build-time API override can be placed in `.env`:
Expand All @@ -244,8 +245,8 @@ There is intentionally no backend URL input in the seller-facing UI.

```bash
cp .dev.vars.example .dev.vars
npm run db:migrate:local
npm run worker:dev
bun run db:migrate:local
bun run worker:dev
```

Local Worker execution is useful for route development and isolated backend testing.
Expand Down Expand Up @@ -300,40 +301,40 @@ ListingOS-Hackathon-Demo-Assets/

| Command | Purpose |
| --- | --- |
| `npm run dev` | Start one clean Expo development-client server |
| `npm run dev:tunnel` | Start the development client in tunnel mode with a cleared cache |
| `npm run android` | Build and run the native Android application |
| `npm run lint` | Run ESLint using the Expo flat configuration |
| `npm run check:docs` | Verify every tracked local Markdown link |
| `npm run typecheck` | Check the mobile and Worker TypeScript projects |
| `npm run doctor` | Run Expo dependency and configuration diagnostics |
| `npm run check` | Run the standard local validation gate |
| `npm run web:verify` | Run application checks, a Worker dry run, and the production web export |
| `npm run verify:submission` | Run application checks, a Worker dry run, and the Android production export |
| `bun run dev` | Start one clean Expo development-client server |
| `bun run dev:tunnel` | Start the development client in tunnel mode with a cleared cache |
| `bun run android` | Build and run the native Android application |
| `bun run lint` | Run ESLint using the Expo flat configuration |
| `bun run check:docs` | Verify every tracked local Markdown link |
| `bun run typecheck` | Check the mobile and Worker TypeScript projects |
| `bun run doctor` | Run Expo dependency and configuration diagnostics |
| `bun run check` | Run the standard local validation gate |
| `bun run web:verify` | Run application checks, a Worker dry run, and the production web export |
| `bun run verify:submission` | Run application checks, a Worker dry run, and the Android production export |

### Builds and releases

| Command | Purpose |
| --- | --- |
| `npm run export:android` | Produce a production Android JavaScript export |
| `npm run export:updates` | Validate iOS and Android OTA bundles locally |
| `npm run eas:update:preview -- --message "description"` | Publish an OTA update to preview testers |
| `npm run eas:update:production -- --message "description"` | Publish an approved OTA update to production |
| `npm run build:android:release` | Build the standalone Android release APK |
| `npm run install:android:release` | Install the standalone APK on a connected Android device |
| `npm run open:android` | Launch the installed ListingOS application |
| `npm run web:serve` | Serve the exported production web application locally |
| `bun run export:android` | Produce a production Android JavaScript export |
| `bun run export:updates` | Validate iOS and Android OTA bundles locally |
| `bun run eas:update:preview -- --message "description"` | Publish an OTA update to preview testers |
| `bun run eas:update:production -- --message "description"` | Publish an approved OTA update to production |
| `bun run build:android:release` | Build the standalone Android release APK |
| `bun run install:android:release` | Install the standalone APK on a connected Android device |
| `bun run open:android` | Launch the installed ListingOS application |
| `bun run web:serve` | Serve the exported production web application locally |

### Worker and data

| Command | Purpose |
| --- | --- |
| `npm run worker:dev` | Start the Worker locally on port 8787 |
| `npm run worker:check` | Type-check and dry-run bundle the Worker |
| `npm run worker:deploy` | Type-check and deploy the Worker |
| `npm run worker:tail` | Stream logs from the deployed Worker |
| `npm run db:migrate:local` | Apply D1 migrations to the local database |
| `npm run db:migrate:remote` | Apply D1 migrations to the configured remote database |
| `bun run worker:dev` | Start the Worker locally on port 8787 |
| `bun run worker:check` | Type-check and dry-run bundle the Worker |
| `bun run worker:deploy` | Type-check and deploy the Worker |
| `bun run worker:tail` | Stream logs from the deployed Worker |
| `bun run db:migrate:local` | Apply D1 migrations to the local database |
| `bun run db:migrate:remote` | Apply D1 migrations to the configured remote database |

## Documentation

Expand Down
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

86 changes: 78 additions & 8 deletions docs/CURRENT_STATE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ListingOS Current Implementation State

Last reconciled against the repository and public endpoints: **July 21, 2026**.
Last reconciled against the repository and public endpoints: **August 10, 2026**.

This file is the documentation source of truth for what is implemented, what is publicly reachable, and what remains external or blocked. If an older plan, script, or checklist conflicts with this file, this file wins.

Expand All @@ -20,7 +20,7 @@ ListingOS is a camera-first seller workflow that turns item photos into evidence
| RevenueCat native billing | iOS/Android SDK integration, offering lookup, purchase, restore, and entitlement sync are implemented | Store catalog availability and final sandbox transactions remain external proof gates | Implemented, not fully production-proven |
| RevenueCat web billing | Hosted purchase-link routing is implemented | EXPO_PUBLIC_REVENUECAT_WEB_PURCHASE_LINKS is empty in checked EAS config | Not ready to claim checkout |
| ListingOS Market web UI | /market and /market/[slug] exist | https://listingos.expo.app/market returned 200 | Public shell is reachable |
| ListingOS Market backend | Publish, feed, detail, inquiry, buyer thread/message, report, block, and rate-event code exists | Public feed returned HTTP 500 on July 21, 2026 | Deployment blocked |
| ListingOS Market backend | Publish, feed, detail, inquiry, buyer thread/message, report, block, and rate-event code exists | Public feed still returned HTTP 500 on August 10, 2026. Root cause confirmed: the marketplace migration is not applied to the remote D1 | Deployment blocked, one operator action from resolution |
| Buyer verification | Configured-code verification exists | No outbound email provider is connected | Controlled demo only |
| Seller Market inbox/replies | Backend does not provide a seller reply workflow and native inbox UI is absent | None | Do not claim |
| Market checkout, escrow, maps, shipping, ratings | Not implemented | None | Explicitly out of scope |
Expand All @@ -38,12 +38,79 @@ Source includes:

Current limitations:

- The deployed public feed currently returns HTTP 500. The exact cause is unconfirmed; remote D1 migration state and Worker logs must be checked before claiming the feature is live.
- The deployed public feed currently returns HTTP 500. The cause is no longer unconfirmed. See
[Market feed 500: confirmed root cause](#market-feed-500-confirmed-root-cause).
- Verification compares a configured demo code. It does not send email.
- Seller inbox/reply UI is not implemented.
- Feed accepts cursor input but currently returns nextCursor: null.
- There is no Market payment, checkout, escrow, map SDK, shipping, rating, or trust-badge system.

## Market feed 500: confirmed root cause

`worker/migrations/0002_marketplace_beta.sql` has not been applied to the remote D1 database.
Every market table is therefore absent in production, and each route that reads one fails.

The Worker source is not at fault. No code change fixes this.

### Evidence

Observed against the deployed Worker on August 10, 2026:

| Request | Result |
|---|---|
| `GET /api/public/market/listings` | 500 |
| `GET /api/public/market/listings?q=test` | 500 |
| `GET /api/public/market/listings/<any-slug>` | 500 |
| `GET /api/public/market/listings?radiusMiles=notanumber` | 400 |
| `GET /health` | 200 |

The 400 is the decisive one. A malformed query is rejected by request validation before any
database read, so routing, the handler, and the error mapper all work. Only the requests that
reach a market table fail, and they fail regardless of which table they touch.

Reproduced locally by applying every migration except `0002_marketplace_beta.sql` to a scratch
database: the feed returns exactly `{"error":"An unexpected server error occurred."}` with HTTP
500, while `/health` still reports `d1Configured: true`. Applying the held-back migration to the
same database returns the feed to HTTP 200 and the detail route to a correct 404.

The migration itself is sound. It applies cleanly in both orderings, and its 13 statements are
all `CREATE TABLE IF NOT EXISTS` or `CREATE INDEX IF NOT EXISTS`, so re-application is safe.

### Why it went unnoticed

`/health` reported `d1Configured: true` throughout, because that field only tested whether the
D1 binding exists. It never queried the database, so a schema gap could not surface. `/health`
now also reports `marketSchemaReady`, which counts the eight market tables in `sqlite_master`
and returns `false` when any is missing.

### Remediation

This requires Cloudflare credentials and must be run by the account owner:

~~~bash
bun x wrangler d1 migrations list seller-ai-db --remote # expect 0002_marketplace_beta.sql pending
bun run db:migrate:remote
~~~

Then confirm, without redeploying the Worker:

~~~bash
curl -fsS .../health | grep -oE '"marketSchemaReady":[^,}]*' # expect "marketSchemaReady":true
curl -s -o /dev/null -w '%{http_code}' .../api/public/market/listings # expect 200
~~~

That `grep` prints nothing at all when the field is absent, as opposed to printing `false`. Deploy
the Worker first only in that no-output case, which means the deployed build predates the probe.

### Note on the duplicate migration prefix

`0002_device_push_tokens.sql` and `0002_marketplace_beta.sql` share a numeric prefix. This was
tested and is **not** the cause: Wrangler tracks applied migrations by filename, so it still
lists a late-added duplicate-prefix migration as pending and applies it correctly.

Do not renumber either file. Wrangler would treat a renamed file as a new migration and
re-run it. Both are idempotent so that would be survivable, but it is needless risk for no gain.

## RevenueCat: exact runtime contract

### Native iOS and Android
Expand Down Expand Up @@ -97,19 +164,21 @@ The following returned HTTP 200 on July 21, 2026:
- https://listingos.expo.app/market
- Worker /health, /app-support, and /privacy

Current red endpoint:
Current red endpoint, re-confirmed August 10, 2026:

- Worker GET /api/public/market/listings returned HTTP 500 with {"error":"An unexpected server error occurred."}.
Root cause confirmed. See [Market feed 500: confirmed root cause](#market-feed-500-confirmed-root-cause).

A 200 web shell does not prove its backing API is healthy.
A 200 web shell does not prove its backing API is healthy. `https://listingos.expo.app/market`
returned 200 on the same day its feed API was returning 500.

## Proof Mode

Proof Mode must be enabled explicitly for the command being run:

~~~bash
EXPO_PUBLIC_PROOF_MODE=true npm run web:export
EXPO_PUBLIC_PROOF_MODE=true npm run web:deploy:production
EXPO_PUBLIC_PROOF_MODE=true bun run web:export
EXPO_PUBLIC_PROOF_MODE=true bun run web:deploy:production
~~~

The convenience script names do not set EXPO_PUBLIC_PROOF_MODE themselves. Keep Proof Mode disabled in native production profiles.
Expand All @@ -122,7 +191,8 @@ Repository code cannot prove these dashboard or publication states:
- RevenueCat offering/package mappings in each store.
- Successful native sandbox purchase, restore, restart, and webhook trace.
- RevenueCat Billing products and non-empty hosted web purchase links.
- Remote D1 migration application and healthy deployed Market feed.
- Remote D1 migration application and healthy deployed Market feed. The required command and
its verification steps are in [Remediation](#remediation); only Cloudflare credentials are missing.
- App review metadata completion and selected builds.
- Public under-three-minute demo video and final submission acceptance.

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@types/react": "~19.2.18",
"eslint": "^9.39.5",
"eslint-config-expo": "~57.0.1",
"expo-doctor": "^1.20.2",
"node-addon-api": "^8.9.2",
"node-gyp": "^12.4.0",
"patch-package": "^8.0.1",
Expand Down Expand Up @@ -77,7 +78,7 @@
"web:deploy:preview": "bun run web:verify && eas deploy --export-dir dist",
"web:deploy:production": "bun run web:verify && eas deploy --prod --export-dir dist",
"web:deploy:proof": "bun run web:deploy:production",
"doctor": "bun x expo-doctor",
"doctor": "expo-doctor",
"eas-build-post-install": "node scripts/prepare-eas-firebase.js",
"generate:icons": "node scripts/generate-icons.js",
"postinstall": "patch-package",
Expand Down
16 changes: 16 additions & 0 deletions worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ app.get("/health", async (c) => {
revenueCatWebhookSigningConfigured: Boolean(c.env.REVENUECAT_WEBHOOK_SIGNING_SECRET?.trim()),
billingEnforcementMode: c.env.BILLING_ENFORCEMENT_MODE === "enforce" ? "enforce" : "observe",
marketDemoEmailVerificationConfigured: isMarketDemoVerificationConfigured(c.env),
marketSchemaReady: await isMarketSchemaReady(c.env),
});
});

Expand Down Expand Up @@ -2497,6 +2498,21 @@ function isMarketDemoVerificationConfigured(env: Bindings) {
return marketDemoVerificationCode(env) !== null;
}

// A present DB binding does not prove the marketplace migration was applied to
// that database. Without this probe a database missing 0002_marketplace_beta.sql
// still reports a healthy D1 while every market route fails with a 500.
async function isMarketSchemaReady(env: Bindings) {
if (!env.DB) return false;
try {
const row = await env.DB.prepare(
"SELECT COUNT(*) AS present FROM sqlite_master WHERE type = 'table' AND name IN ('market_listings', 'market_threads', 'market_messages', 'market_buyer_identities', 'market_buyer_sessions', 'market_blocks', 'market_reports', 'market_rate_events')",
).first<{ present: number }>();
return (row?.present ?? 0) === 8;
} catch {
return false;
}
}

async function sha256Hex(value: string) {
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
return bytesToHex(digest);
Expand Down
Loading