# SaaS VOC Server Independent backend template for the domestic ecommerce VOC product. The first case workspace is Demashi on JD. This repository does not connect to the cross-border production database and does not contain any Fmode, Parse, PostgreSQL, or supplier credential. ## Current state Completed through 2026-07-24: - Independent Git repository and Node.js 22 / TypeScript service baseline. - Independent PostgreSQL schema with workspace, source, product, metric, relation, review, import, and sync-job tables. - Independent Parse Server application configuration on the same dedicated database. - Strict startup validation for every required credential. - `GET /health` database readiness check. - `POST /api/domestic-voc/sync` idempotent manual-sync queue entry. - `GET /api/domestic-voc/jobs/:id` sync-job status. - `GET /api/domestic-voc/snapshot` response compatible with the frontend `DomesticDataset` interface. - Server-side Fmode gateway client with timeout, retry, and credential redaction behavior. - Deterministic identities for products, relations, reviews, and daily sync requests. - Bounded batch importer for the normalized Demashi dataset. - Database-free local demo runtime for frontend integration before infrastructure is provisioned. - Workspace membership and `owner` / `admin` / `analyst` / `viewer` authorization. - Parse session authentication for production and a fixed local identity for database-free development. - Cursor-paginated product, relation, review, import, sync-job, analysis, action, alert, and audit APIs. - Pending analysis requests, action workflows, alerts, and auditable write operations without fabricated AI output. - Configurable local workspace identity instead of a Demashi-only runtime branch. - Manual sync retry/cancel operations plus automatic recovery for stale processing jobs. - Owner-only owner changes, last-owner protection, and workspace-member action assignment validation. - Parse REST storage adapter for environments where only a Parse Server URL and application credentials are available. - Master-key-only `Voc*` class schemas, resumable bounded imports, direct count verification, and a Parse-backed sync worker. - Direct streaming import from the source Demashi workbook without a frontend-generated JSON intermediate. - Idempotent competitor detail backfill through the company ecommerce relay, with mapped placeholders preserved when a request fails. The current case import resolves to 2,817 operating products, 37 mapped competitor products, 9,717 daily metrics, 40 relations, and 0 reviews. All 37 competitor product details were persisted through the company relay on 2026-07-24. The PostgreSQL importer may create internal `relation_stub` rows to preserve foreign keys; the Parse REST model stores denormalized relation identities and therefore does not create those extra catalog products. The worker calls JD product-detail and review paths only through the company `/api/voc-e-commerce` gateway. It never sends a browser request to a supplier endpoint and never returns raw gateway errors or credentials. ## Architecture ```text Saas-voc frontend -> saas-voc-server -> shared API and authorization contract -> local in-memory repository (development) -> dedicated PostgreSQL repository (production, voc schema) -> external Parse REST repository (shared development or managed Parse) -> dedicated Parse application (production authentication) -> sync queue -> existing /api/voc-e-commerce gateway ``` The browser only calls this service. `FMODE_API_KEY` is used in the server-side `Authorization` header and is never returned to the browser or included in a request URL. Registered JD gateway contracts: ```text GET /api/voc-e-commerce/jd/get-item-detail/v1?itemId={productId} GET /api/voc-e-commerce/jd/get-item-comments/v1?itemId={productId}&page={page} ``` The first worker run defaults to one review page. `JD_REVIEW_MAX_PAGES` can raise the bounded limit after quota and response validation. ## Runtime Use Node.js 22.13 or newer within the Node 22 release line. The repository intentionally pins Node 22 because Parse Server publishes explicit supported runtime ranges. Required configuration is listed in `.env.example`. Empty secrets are rejected before the HTTP server starts. `STORAGE_DRIVER=parse_rest` requires the external Parse URL, application id, and master key but no direct database connection. `STORAGE_DRIVER=postgres` additionally requires `DATABASE_URL`, a maintenance key, and optionally a separate `MIGRATION_DATABASE_URL`. Never commit live credentials. ### Parse REST runtime Use this mode when PostgreSQL is not exposed and the project must connect through an existing Parse Server REST API: ```powershell $env:STORAGE_DRIVER = 'parse_rest' $env:PARSE_SERVER_URL = 'https://parse.example.com/parse' $env:PARSE_APP_ID = '' $env:PARSE_MASTER_KEY = '' $env:API_AUTH_MODE = 'disabled' # isolated local development only $env:FMODE_API_KEY = '' npm run bootstrap:parse-rest -- "E:\workspace\Saas-voc\src\assets\data\demashi-summary.json" demashi npm run import:workbook:parse-rest -- "E:\path\to\德玛仕产品及竟品收集0721.xlsx" demashi npm run sync:competitors:parse-rest -- demashi --concurrency=3 npm run verify:parse-rest -- demashi jd npm run dev ``` The bootstrap command creates or reconciles 14 isolated `Voc*` classes, seeds the workspace/member/source records, imports the normalized case in request-size-bounded batches, and is idempotent for the same source hash and verified counts. The workbook command performs the same bounded import directly from Excel using a streaming reader. Competitor sync creates all mapped competitor records first, then skips already completed details on later runs. The verification command checks the schema set, class-level permissions, workspace/import readiness, exact own/competitor/detail counts, and denial of app-id-only reads. See `docs/parse-rest-schema.md` for the class contract. ### Local frontend integration without a database The local demo runtime has no PostgreSQL, Parse, or gateway credential dependency. From this repository run: ```powershell npm ci npm run start:local ``` It loads `../../Saas-voc/src/assets/data/demashi-summary.json` by default and listens on `http://127.0.0.1:4400`. Override the source only when the repositories are stored elsewhere: ```powershell $env:LOCAL_DATASET_PATH = 'E:\path\to\demashi-summary.json' npm run start:local ``` Local demo `sync` requests create queryable in-memory completion records that validate whether requested products exist in the packaged dataset. They do not collect external data or persist anything. Production collection and persistence remain in `npm run dev`. Local mode injects the fixed `local-admin` owner. It is intentionally database-free and must not be internet-facing. Set `LOCAL_WORKSPACE_ID` when using a packaged dataset under a workspace other than `demashi`; omitted API workspace ids then resolve to that configured default. ### Database-backed runtime Local database: ```powershell $env:POSTGRES_PASSWORD = '' docker compose up -d postgres ``` The compose database listens only on `127.0.0.1:54329`. A local `DATABASE_URL` therefore uses database `saas_voc` on port `54329`. Install, migrate, and run: ```powershell npm ci npm run migrate npm run dev ``` Import the current normalized case after migration: ```powershell npm run import:dataset -- "E:\workspace\Saas-voc\src\assets\data\demashi-summary.json" ``` `001_initial_domain.sql` creates the domestic VOC catalog and sync tables. `002_saas_platform.sql` adds members, analysis runs, actions, alerts, and audit history. The migration runner records each file in `voc.schema_migration` and executes each unapplied migration in its own transaction. ### Authentication and first administrator Use `API_AUTH_MODE=parse` in production. API clients send either `X-Parse-Session-Token` or `Authorization: Bearer `. The backend validates the session through `PARSE_SERVER_URL/users/me` and never sends a Parse master or maintenance key to the browser. For the first production start, set `SAAS_BOOTSTRAP_ADMIN_USER_ID` to the Parse user object id and optionally set its email and display name. The service upserts that user as owner of `SAAS_DEFAULT_WORKSPACE_ID`. Clear the bootstrap variables after successful verification so a later restart cannot silently restore that account's owner role. `API_AUTH_MODE=disabled` is available for isolated database-backed development only. It uses `LOCAL_AUTH_USER_ID`, `LOCAL_AUTH_USER_EMAIL`, and `LOCAL_AUTH_USER_NAME`, and must not be enabled in a public environment. Default local API address: ```text http://127.0.0.1:4400 ``` ## API Queue a JD product and review sync: ```http POST /api/domestic-voc/sync Idempotency-Key: optional-client-key Content-Type: application/json { "workspaceId": "demashi", "platform": "jd", "productIds": ["11266507445"], "scopes": ["product", "reviews"] } ``` When `Idempotency-Key` is absent, the service derives a stable key from workspace, platform, products, scopes, and UTC date. The same daily request returns the same job. Read status and frontend data: ```text GET /api/domestic-voc/jobs/{jobId} POST /api/domestic-voc/jobs/{jobId}/retry POST /api/domestic-voc/jobs/{jobId}/cancel GET /api/domestic-voc/snapshot?workspaceId=demashi&platform=jd ``` Catalog and operational APIs: ```text GET /api/domestic-voc/products GET /api/domestic-voc/products/{productId} GET /api/domestic-voc/products/{productId}/reviews GET /api/domestic-voc/relations GET /api/domestic-voc/jobs GET /api/domestic-voc/jobs/{jobId}/events GET /api/saas/context GET /api/saas/workspaces GET|PUT /api/saas/workspaces/{workspaceId}/members[/userId] GET /api/saas/workspaces/{workspaceId}/data-sources GET /api/saas/workspaces/{workspaceId}/imports GET|POST /api/saas/workspaces/{workspaceId}/analyses GET|POST|PATCH /api/saas/workspaces/{workspaceId}/actions[/actionId] GET|POST|PATCH /api/saas/workspaces/{workspaceId}/alerts[/alertId] GET /api/saas/workspaces/{workspaceId}/audit ``` List endpoints use opaque cursor pagination (`limit` plus optional `cursor`). Analysis creation returns a truthful `pending` record; an analysis worker is not included yet. An empty database returns a valid empty dataset. It does not invent reviews, ratings, sentiment, pain points, or AI output. ## Database rules - Runtime identities are enforced by composite unique constraints, not by check-then-insert logic. - Import and sync writers must use `INSERT ... ON CONFLICT` in batches. - Foreign-key columns and common workspace/date filters are indexed. - Workers claim jobs with `FOR UPDATE SKIP LOCKED`. - Worker startup recovers processing jobs older than `SYNC_JOB_STALE_AFTER_MS`; exhausted jobs become failed and eligible jobs return to the queue. - External HTTP requests must run outside database transactions. - Workspace membership is checked on every domestic and SaaS business route. - Disabled workspaces cannot pass authorization even when a membership row remains active. - Owner and admin can manage ordinary members and read audit history; only owners can change owner membership, and the final active owner is protected. Analyst can run sync/analysis and manage actions/alerts; viewer is read-only. - Action assignees must be active members of the same workspace. - Member, sync, analysis, action, and alert writes append an audit entry. - Production deployments must use a pooled, least-privilege runtime role. Migration credentials should be separate from runtime credentials. - PostgreSQL DDL is always an explicit migration step. Parse REST mode reconciles only the isolated `Voc*` class schemas at startup through the schema REST API. ## Infrastructure handoff When the dedicated database and server are available, no route or frontend contract rewrite is required: 1. Set `DATABASE_URL` to the least-privilege runtime role and `MIGRATION_DATABASE_URL` to the schema owner. 2. Set independent Parse keys, `PARSE_SERVER_URL`, and `API_AUTH_MODE=parse`. 3. Set the company relay base URL/key, worker limits, and exact frontend `CORS_ORIGINS`. 4. Run `npm ci`, `npm run migrate`, then `npm run import:dataset -- `. 5. Bootstrap the first owner, start the service, verify `/health`, `/api/saas/context`, snapshot totals, and one bounded sync job. 6. Clear the bootstrap administrator variables and place TLS/same-origin proxying in front of `/parse` and `/api`. ## Security status `parse-server@9.10.0` replaces the initially evaluated v8 line, removing all high and critical audit findings. A `ws@8.21.0` override is retained because Parse currently pins an older vulnerable patch. The remaining 14 audit findings are moderate transitive dependencies in Parse push/Firebase and redirect support plus ExcelJS's nested UUID package; push and LiveQuery are not configured in this template. Re-run `npm audit --omit=dev` before each deployment and do not use `npm audit fix --force`, which currently proposes breaking Parse/ExcelJS downgrades. ## Verification ```powershell npm run build npm test npm run verify:parse-rest -- demashi jd npm audit --omit=dev ``` Current result: - TypeScript build: passed. - Unit, REST client, adapter, worker recovery, local-demo, authentication, RBAC, cursor, workflow, audit, and HTTP contract tests: 35 passed. - Production dependency audit: 0 critical, 0 high, 14 moderate. - Company gateway health: HTTP 200 on 2026-07-23. - Credentialed live JD product-detail request: passed through the company gateway for product `11266507445`; the adapter extracted the product id, title, and brand from the live double-`data` envelope. - Credentialed live JD review request: authenticated successfully and returned an empty first page with next-page metadata. A non-empty review sample is still required before review evidence can be accepted. - Live credentials are absent from the repository, fixtures, logs, and Git history. No supplier endpoint is contacted directly. - Parse REST development verification: 14/14 `Voc*` schemas present, master-key-only access confirmed, 2,817 own products, 37 competitor products with 37 details ready, 9,717 metrics, 40 relations, and 0 reviews. - End-to-end local API mode: `/health`, SaaS context, full snapshot, frontend proxy, desktop navigation, and 390x844 responsive rendering passed against the Parse REST store. - PostgreSQL integration: pending a newly provisioned database. The local Docker CLI is installed but its engine was unavailable on 2026-07-23; no existing database was contacted. See `TASKS.md` for the implementation sequence and acceptance boundary.