Overview
Charlie is built on security-first principles: no customer PII, no payment data, minimal Shopify API permissions, and full data residency within the Shopify and Cloudflare ecosystem.
Data governance
What Charlie stores
Charlie stores only operational configuration in its own database (Cloudflare D1 — SQLite).
The complete database schema contains ten tables, none of which hold customer PII:
| Table | Purpose | PII |
|---|
sessions | Shopify OAuth session tokens, app settings | No |
locations | Location metadata, capacity settings, coordinates | No |
safety_stock_rules | Merchant-defined inventory buffer rules | No |
events | Webhook deduplication log | No |
bulk_operations | Shopify bulk operation tracking | No |
migration_runs | Internal migration state | No |
revenue_attribution_rules | Revenue attribution configuration per location | No |
analytics_settings | Analytics display preferences | No |
inventory_exports | Export job tracking and delivery status | No customer PII |
activity_logs | Audit trail of configuration changes | No |
Charlie contains no customer PII, no order content, no payment information. This is enforced at the Shopify API scope level — Charlie does not request read_customers access.
What Charlie writes to Shopify
Charlie computes and writes back operational values to Shopify
metafields. All values are merchant configuration or computed
inventory data — no customer data is involved.
Variant metafields
| Metafield | Type | Purpose |
|---|
inventory.levels | JSON | Per-location sellable and safety stock quantities |
inventory.compact | String | Optimized format consumed by Shopify Functions |
inventory.fulfillable | Integer | Total fulfillable quantity across locations |
inventory.available | Boolean | Whether any stock is available |
inventory.safety_stock | JSON | Per-location safety stock overrides |
Product metafields
| Metafield | Purpose |
|---|
inventory.safety_stock | Safety stock overrides for all variants |
inventory.updated_at | Timestamp of last inventory sync |
inventory.available | Boolean availability across all variants |
inventory.first_available_variant | Reference to first in-stock variant |
Location metafields
| Metafield | Purpose |
|---|
location.type | WAREHOUSE or STORE classification |
location.tags | Merchant-defined tags for routing rules |
location.at_capacity | Whether location has reached daily order limit |
location.capacity_mode | BLOCK or PRIORITIZE |
location.daily_orders_quota_left_ratio | Float 0–1, remaining daily capacity |
location.can_fulfill | Whether location is active for fulfillment |
location.shipping_enabled | Whether location ships online orders |
What Charlie never touches
The following data stays exclusively within Shopify and is never
accessed, processed, or stored by Charlie:
- Customer PII (names, emails, addresses, phone numbers)
- Payment and billing information
- Order content and transaction history
- Customer purchase history
Data ownership and portability
| Aspect | Detail |
|---|
| Rules storage | Merchant rules stored in Charlie’s Cloudflare D1 database |
| Computed values | Inventory and capacity data written to Shopify metafields |
| Ownership | 100% merchant-owned — all metafield values accessible via Shopify Admin API |
| Portability | No proprietary formats — standard JSON metafields |
| Uninstall | All Charlie data deleted on shop redact; metafield values persist in Shopify |
Shopify API permissions
Charlie requests only the scopes required for omnichannel fulfillment
operations. No customer data scope is requested or used.
| Scope | Purpose |
|---|
read_inventory | Read stock levels for sellable quantity computation |
read_locations | Read merchant location list |
write_locations | Write location metafields (type, tags, capacity state) |
read_products | Read variant data for safety stock computation |
write_products | Write inventory metafields on variants and products |
read_orders | Read fulfillment order context for routing |
read_assigned_fulfillment_orders | Access orders assigned to locations |
write_assigned_fulfillment_orders | Update fulfillment assignments |
read_merchant_managed_fulfillment_orders | Read merchant-managed orders |
write_merchant_managed_fulfillment_orders | Route and update fulfillment orders |
read_fulfillment_constraint_rules | Read existing routing constraint rules |
write_fulfillment_constraint_rules | Write routing and capacity constraint rules |
read_shipping | Read shipping profiles |
write_shipping | Update shipping profiles for routing rules |
read_third_party_fulfillment_orders | Read third-party fulfillment context |
read_validations | Read cart validation extensions |
No read_customers scope is requested or used at any point.
Charlie has zero API access to customer PII.
Security controls
Authentication and session management
- All admin routes enforce
shopify.authenticate.admin(request)
before any processing — unauthenticated requests are rejected
immediately
- Sessions stored in Cloudflare D1 using Shopify’s official
session storage adapter
direct_api_mode = "online" — no long-lived offline tokens
- Access tokens stored server-side only, never exposed to
client-side code
Multi-tenancy isolation
Every database query is scoped by shop domain at the ORM layer.
No query executes without an explicit WHERE shop = ? filter
derived from the authenticated Shopify session. This is enforced
in code on every data access function — not by convention alone.
Webhook security
| Control | Implementation |
|---|
| HMAC verification | All Shopify webhooks verified via Shopify SDK before processing — invalid signatures return HTTP 401 |
| Replay attack prevention | Webhook IDs stored in events table with UNIQUE constraint — duplicate events are rejected |
| Async processing | Webhooks enqueued to Cloudflare Queues — prevents timeout-based abuse |
| Dead-letter queues | Failed jobs automatically routed to charlie-background-queue-dlq and charlie-webhook-queue-dlq |
Applied globally to all requests via Hono secureHeaders() middleware:
| Header | Value |
|---|
X-Content-Type-Options | nosniff |
X-XSS-Protection | 1; mode=block |
Referrer-Policy | strict-origin-when-cross-origin |
X-DNS-Prefetch-Control | off |
X-Download-Options | noopen |
X-Permitted-Cross-Domain-Policies | none |
Content-Security-Policy and X-Frame-Options are intentionally
omitted. Charlie runs as a Shopify embedded app inside an iframe —
Shopify manages these restrictions at the platform level.
Rate limiting
Charlie implements a token-bucket rate limiter for all outbound
Shopify API calls:
- Max 20 requests/second toward Shopify API
- Max 10 concurrent requests per worker instance
- Queue-based backpressure — requests wait rather than fail
under load
Encryption
| Layer | Implementation |
|---|
| In transit | TLS enforced by Cloudflare on all traffic |
| At rest | Cloudflare D1 encryption at rest |
| Access tokens | Stored in D1 only, never exposed client-side |
Error monitoring
Sentry is used for operational error monitoring. Error payloads
contain stack traces and request context only — no customer PII,
no access tokens, no session content. Sentry is disabled in
development environments.
GDPR compliance
Charlie implements all three mandatory Shopify GDPR compliance
webhooks:
Shop redact (shop/redact)
Triggered when a merchant uninstalls and requests data deletion.
Charlie deletes all shop-specific data atomically across all database tables.
Customer data request (customers/data_request)
Charlie acknowledges this webhook and returns no data — by design.
The app stores no personal customer data to return. This is
documented explicitly in the codebase:
This app does NOT store any personal customer data.
We only store shop-level configuration (locations, safety stock
rules, etc.). No customer PII, order details, or personal
identifiers are persisted. If this app ever stores customer data
in the future, this service must be updated.
Customer redact (customers/redact)
No-op by design — Charlie stores no customer-level data to redact.
Infrastructure and sub-processors
| Provider | Role | Certification |
|---|
| Shopify | Commerce platform, data source of truth | SOC2 Type II |
| Cloudflare Workers | Serverless compute | SOC2 Type II |
| Cloudflare D1 | Primary database (SQLite) | SOC2 Type II |
| Cloudflare KV | Cache — shop info and location metadata | SOC2 Type II |
| Cloudflare Queues | Async background job processing | SOC2 Type II |
| Cloudflare R2 | Object storage — database backups, inventory exports | SOC2 Type II |
| Cloudflare Analytics Engine | Fulfillment analytics data | SOC2 Type II |
| Sentry | Error monitoring | SOC2 Type II |
| PostHog | Product analytics | SOC2 Type II |
| Resend | Transactional email (export notifications) | SOC2 Type II |
| BetterStack | Uptime monitoring and status page | SOC2 Type II |
| Mantle | Subscription billing management | — |
| Google Maps Time Zone API | Timezone resolution from location coordinates | — |
Charlie does not currently hold SOC2 certification.
All compute and storage infrastructure runs on Cloudflare (SOC2 Type II certified).
Risk summary
| Risk | Assessment |
|---|
| Data breach exposure | Minimal — no customer PII in Charlie systems |
| Payment data risk | None — all transactions remain within Shopify |
| API over-permissioning | Low — no customer scope; write scopes scoped to inventory and fulfillment metafields |
| Vendor lock-in | Low — merchant-owned metafields in standard JSON format |
| Uninstall risk | Low — complete data deletion via shop redact webhook |
| IT burden | Zero — no on-premise component, no integration maintenance |