Skip to main content

Pcnaid Safe Bulk architecture

Runtime boundary

The application is a TypeScript Cloudflare Worker using Hono. Shopify App Bridge supplies staff session tokens, the Admin GraphQL API supplies catalog and subscription truth, the Worker connects to PostgreSQL, KV holds bounded rate-limit and recovery markers, and R2 holds import and privacy-export artifacts. PostgreSQL is authoritative for workflows, tenancy, replay control, billing history, and audit state.

Tenant and identity model

The verified dest in a Shopify session token resolves the installed shop. The Worker sets both shop UUID and domain PostgreSQL session variables before tenant SQL. All tenant tables enable and force row-level security. Three narrow security-definer functions are owned by a dedicated role; runtime SQL uses a separate application role that cannot bypass RLS. The verified token sub is the immutable staff identity. Static assignments in SAFE_BULK_AUTHZ_POLICY and tenant staff_roles map it to one of four roles: operator, approver, executor, or admin. Route-level action checks occur after session authentication and before paid or mutating service calls. Shopify OAuth access and refresh tokens are encrypted with AES-GCM before database storage. The encryption key supports rotation: a previous key stays valid for decryption while new writes use the current key.

Governed change model

bulk_projects group data_sources, change_sets, and playbooks. A change set owns immutable intent rows in change_items, validation results, approvals, execution jobs, and rollback snapshots. Every cross-table reference includes shop_id; composite foreign keys reject a valid child UUID paired with the wrong tenant. CSV import and playbook paths converge at the same diff, risk, and approval service. This keeps scheduling from bypassing governance.

Change-set lifecycle

  • Import enqueues a durable background job that reads Shopify product snapshots in batches, computes field-level diffs, and persists items.
  • Approval requires a configurable number of distinct approvers depending on risk score and critical items.
  • Execution claims an approved set atomically, saves before-value snapshots, applies each item through Shopify, and records outcomes.
  • Rollback reverses executed items from their captured snapshots.
  • AI review is available during the approval phase, sampling up to 200 items for a draft assessment.

Billing model

Two plan tiers are available: shops.subscription_* is a deliberately disposable current-state projection. Shopify currentAppInstallation.activeSubscriptions is queried on callbacks, cancellations, and paid operations. A deployment setting controls whether paid writes enforce an active subscription. Verified Shopify development stores receive Growth access through a developer entitlement override. shop_billing_lifecycle records first trial use, checkout reservations, and the last selected handle separately from the subscription projection to prevent granting a second trial or reducing a paid tier.

Reliability model

Database uniqueness controls OAuth installs, webhook deliveries, idempotency requests, seed rows, and approvals. Webhook rows use a bounded processing lease so a crash after insertion is retryable. CSV imports are durable, resumable background jobs. Upload enforcement of plan limits (byte and row ceilings) happens before any R2 write or database write. The worker processes imports in atomic 500-row batches, committing diff items together with row offset progress. A recovery-tenant marker is registered before any external or database write. The scheduler runs every ten minutes and handles:
  • Reclaiming queued or stale-running jobs
  • Running due scheduled playbooks
  • Lifecycle maintenance (webhook backfill, deferred redaction)
  • Expired privacy-export artifact cleanup

Scheduler and durable jobs

A key-value store holds tenant registry markers. PostgreSQL atomically claims due playbooks, execution and rollback jobs, and durable import jobs with FOR UPDATE SKIP LOCKED. A request starts execution immediately for responsive feedback, while CSV imports return HTTP 202 and process asynchronously. Every running claim has an opaque lease token. Each committed batch conditionally advances its expected next_offset under that token before inserting items. Execution and rollback heartbeat the token at least once per item. Recovery caps attempts at five and removes inactive markers.

GraphQL rate-limit handling

Admin GraphQL requests route through a unified client that monitors Shopify’s leaky-bucket rate limiter via extensions.cost.throttleStatus. The client pre-waits when projected quota is below estimated cost. Interactive execution uses a 5-second request budget per call plus a 20-second total inline budget; background recovery uses the 60-second per-call job budget. GraphQL THROTTLED and HTTP 429 responses are retried. Mutations that are value-idempotent (including handle-keyed productSet) set idempotent: true for safe retry.

Browser rendering boundary

@pcnaid/shopify-worker-core supplies the shared browser runtime and HTML-escaping primitive. The UI table helper treats every heading, caption, and ordinary cell as text. A cell may contain markup only when application code constructs it with the runtime’s branded trustedHtmlCell() helper. Link destinations pass through appRelativePath() to prevent javascript: and cross-origin links.

Public routes

The app exposes these public routes:
  • GET /health — returns app identity, build SHA, and billing configuration status
  • GET /sitemap.xml — XML sitemap with public support page URLs
  • GET /robots.txt — robot exclusion rules
  • GET /support — public support page
  • GET /privacy and GET /terms — HTTP 301 redirects to docs.pcnaid.com
Pcnaid Documentation › Shopify Ecosystem › Pcnaid Safe Bulk