# ZShip Full Context > Expanded public context for LLMs and AI agents. This resource contains public product, documentation, blog, and store information only. Site: https://zship.ai ## Key Facts - ZShip is built around 10+ independent Cloudflare Worker services for auth, billing, support, blog, CDN, site operations, prompts, check-in, AI, and delivery. - The app_key tenant model lets one foundation power multiple product sites while keeping project data and operations scoped. - The customer platform includes orders, licenses, GitHub access, downloads, support tickets, store purchases, credits, changelog, roadmap, docs, blog, and showcase pages. - ZShip is optimized for builders selling reusable AI SaaS products, templates, components, and value-added services rather than only a single landing page. - The default deployment stack uses Nuxt, Astro, Cloudflare Pages, Workers, D1, KV, R2, and service bindings. ## Public Pages ### ZShip - Cloudflare AI SaaS Development Template URL: https://zship.ai ZShip is the Cloudflare-native, multi-tenant AI SaaS development template for launching products, managing paid delivery, and reusing growth infrastructure. ### ZShip Lifetime Pricing URL: https://zship.ai/pricing One clear $399 lifetime license for the full ZShip commercial SaaS platform, including checkout, licensing, GitHub delivery, downloads, support, and updates. ### ZShip Core Platform Capabilities URL: https://zship.ai/capabilities Provider routing, guest activation, welcome credits, Image Agent, customer delivery, and Cloudflare-native operations in one AI SaaS platform. ### ZShip Cookie Policy URL: https://zship.ai/cookie-policy Learn which essential cookies ZShip uses for secure sessions, preferences, and core site functionality. ### ZShip vs ShipAny vs MkSaaS URL: https://zship.ai/compare Compare ZShip with ShipAny and MkSaaS across architecture, Cloudflare deployment, delivery workflows, customer operations, docs, and AI SaaS launch readiness. ### ZShip Docs URL: https://zship.ai/docs Deployment, customization, service, and operations documentation for launching AI SaaS products with ZShip. ### ZShip Store URL: https://zship.ai/store Browse ZShip components, templates, code snippets, and blocks that extend the customer platform, speed up product launches, and reuse proven workflows. ### ZShip Blog URL: https://zship.ai/blog Read practical ZShip product notes, launch workflows, Cloudflare deployment guides, and SaaS operations lessons for building faster AI SaaS products. ### ZShip Showcase URL: https://zship.ai/showcase Explore real AI SaaS sites and customer platforms built with ZShip, including launch examples, product workflows, and Cloudflare-native deployments. ### ZShip Changelog URL: https://zship.ai/changelog Follow ZShip product updates, release notes, fixes, and platform improvements across the app, docs, checkout, customer delivery, and Cloudflare stack. ### ZShip Roadmap URL: https://zship.ai/roadmap Track planned ZShip platform features, in-progress improvements, and completed updates across checkout, delivery, support, docs, store, and operations. ## Documentation ### Codebase URL: https://zship.ai/docs/codebase ZShip project structure: core apps and backend services. This page describes the core delivery structure: apps/web-astro, apps/admin, and the 11 backend Workers. Core directory layout zship/ ├── apps/ │ ├── web/ # User-facing site (Astro + Vue) │ └── admin/ # Admin console (Nuxt 4 + Nuxt UI) ├── backend/ │ ├── node1-auth-service/ # Auth, users, credits, API keys │ ├── node2-support-service/ # Tickets, email, feedback │ ├── node3-pay-service/ # Payments, pricing, webhooks │ ├── node4-notify-service/ # Bark, Feishu, DingTalk │ ├── node5-blog-service/ # Blog CMS │ ├── node6-cdn-service/ # File upload, CDN │ ├── node7-site-service/ # Banners, friend links, waitlist │ ├── node8-prompt-service/ # Prompt library │ ├── node9-checkin-service/ # Check-in rules, rewards │ ├── node10-ai-service/ # AI gateway, chat, sessions │ └── node11-email-service/ # Email providers, app routes, delivery logs ├── packages/ # Shared packages (used by core delivery) │ ├── dashboard/ # Dashboard components, brand config │ ├── backend-utils/ # Worker / admin utilities │ ├── blog-engine/ # Blog markdown pipeline │ ├── notify-utils/ # Notification helpers │ └── nuxt-admin-layer/ # Admin Nuxt layer └── scripts/ # Dev Console, deploy scripts Backend services (core) Service Directory Role Auth node1-auth-service Login, register, users, credits, API keys Support node2-support-service Tickets, feedback, email Pay node3-pay-service Stripe, Creem, Dodo Payments, Waffo Pancake, pricing, webhooks Notify node4-notify-service Push notifications Blog node5-blog-service Blog CMS CDN node6-cdn-service File upload, storage Site node7-site-service Banners, friend links, waitlist Prompt node8-prompt-service Prompt templates Check-in node9-checkin-service Check-in rules, rewards AI node10-ai-service AI gateway, multi-model chat Email node11-email-service Cloudflare Email Service / Resend providers, app routes, delivery logs Value-added (not in core) - zship-provider1-service — AI provider gateway (KIE, WaveSpeed AI), purchasable separately - apps/landing, apps/web-nuxt — Optional marketing / alternate frontends Next steps - Deployment Guide — Deploy core stack ### Important Notes URL: https://zship.ai/docs/important-notes Rules you must follow: do not modify official apps and backend services. You must follow these rules. Official code will continue to be maintained and updated by us. Do not modify Frontend apps - apps/admin - apps/web-astro These are official templates. If you modify them directly, it becomes difficult to merge future upstream updates cleanly. Recommended: Copy apps/web-astro to your own project, for example apps/ai-saas, and customize the copy. If you copy apps/web-astro, also do these 4 things This is the current correct workflow for a new brand site: - Create the tenant in Admin first - Admin Projects is the tenant registry - Editing frontend files alone does not register a tenant - Add or update zship.app.json in the copied app - Set appKey to the Admin tenant app_key - Set domain, siteUrl, and brand metadata there - Change wrangler.toml - Update the name field for the copied frontend - Otherwise you may overwrite the original web deployment target - Redeploy the copied app as a separate site - Bind its own domain - Push its own env and secrets Short version: - Admin owns tenant creation - zship.app.json owns frontend-to-tenant mapping - wrangler.toml owns the Worker deploy target Backend services Do not modify any of these official services: - backend/node1-auth-service - backend/node2-support-service - backend/node3-pay-service - backend/node4-notify-service - backend/node5-blog-service - backend/node6-cdn-service - backend/node7-site-service - backend/node8-prompt-service - backend/node9-checkin-service - backend/node10-ai-service - backend/zship-provider1-service These are official services. We will update them with new features and fixes. Do not change them directly. Security and secrets - Never commit .env files, signing keys, or cookie secrets to a public repo. Use Cloudflare Secrets, wrangler secret put, or Dev Console push for production. - Rotate ADMIN_SECRET, JWT secrets, and payment/provider keys on a sensible schedule, then redeploy the Workers that read those values. - Prefer operator accounts and RBAC in admin (docs) instead of sharing the root secret across many people. - Layer Cloudflare protections such as WAF and IP rules on top of least-privilege access for admin and sensitive APIs. ### Introduction URL: https://zship.ai/docs/introduction ZShip overview: Cloudflare-native multi-tenant SaaS foundation for AI-powered sites. ZShip is a Cloudflare-native, multi-tenant foundation for launching AI-powered SaaS sites. It provides a ready-made stack: user auth, payments, AI gateway, blog, check-in, and more. Official site and live demos Resource URL Marketing site & documentation https://zship.ai/zh/ Astro demo https://web.zship.ai/ Admin console (demo login page) https://admin.zship.ai/login Demo account (read-only): test@zship.ai / test123123 — view access only, for exploring the admin UI. Do not use it to change production data or sensitive settings. Quick link (prefill email): Login with ?email=… pre-fills the demo address; enter the password manually (never put passwords in URLs). Other admin locales use a path prefix, e.g. Simplified Chinese: https://admin.zship.ai/zh-CN/login. Email magic link (passwordless): On Operator login, use Email me a link — a one-time message is sent; opening the link signs you in via magic_token. For self-hosted stacks, set ADMIN_PUBLIC_ORIGIN on node1-auth-service, apply migration 0012_admin_magic_link, and ensure node2 can send mail (same path as forgot-password). See backend/node1-auth-service/README.md. Core delivery What you deploy and run: Type Components Role Frontend apps/web-astro User-facing site: Landing, Pricing, Blog, Dashboard Frontend apps/admin Admin console: users, payments, content, config Backend node1-auth ~ node11-email 11 Cloudflare Workers Official post-purchase site app.zship.ai: orders, licensing, GitHub delivery, downloads, and support — see what’s available. Value-added services Not included in core deployment. Can be purchased separately: - AI Provider Service (zship-provider1-service): Unified gateway for external AI providers. Currently supports KIE, WaveSpeed AI. See Value-Added Services. What you get - User auth (email, Google OAuth, API keys) - Payments (Stripe, Creem, Dodo Payments, Waffo Pancake) - AI gateway (node10-ai, multi-model chat) - Blog CMS, check-in, notifications, file storage - Multi-language (i18n), branding customization Retention and engagement If you want longer sessions, higher visit frequency, and long-term retention, the lever is usually not “ship more features,” but make the first experiences users actually notice feel great. ZShip ships three high-signal capabilities for acquisition and active usage: - Guest access (no sign-up) — Let people try before they commit to an account. - Free credits — Give new users something real to spend on first visit. - Daily check-in — A familiar loop for return visits and stickiness. For new visitors, try first, register later tends to lift entry rate and time on site. Guest access plus free credits extend first-session dwell time and help turn a casual visit into a genuine trial. Daily check-in gives users a reason to come back every day, reinforces the habit of opening your product, and supports steadier day-to-day traffic (DAU). Technical details: Check-in integration. Next steps - Important Notes — Rules you must follow - Codebase — Project structure - Deployment Guide — Deploy web + admin + 11 Workers ### CLI: Before You Start URL: https://zship.ai/docs/cli-01-before-start Prerequisites for the CLI path: accounts, source access, local tools, and Wrangler login. This chapter covers preparation before you run any deployment commands. Do not skip it. 1. What you are deploying ZShip is not a single-page project. It includes: - 11 backend Workers - 2 frontend apps (web and admin) - 10 D1 databases - 1 KV namespace (payment service) - 1 R2 bucket (CDN service) You are deploying a Cloudflare-based system, not just one website. 2. Accounts you need At minimum: - A Cloudflare account - A copy of the Git repository - A machine with Node.js Optional: - Stripe account (for payments) - Creem account (for Creem payments) - Dodo Payments account (for Dodo payments) - Waffo Pancake account (for Waffo payments) - Google OAuth app (for Google login) 3. How to get the source code Option A: GitHub Fork - Fork the official template into your GitHub account or organization - Clone your fork locally Advantages: Keeps upstream relationship; easier to sync future updates. Option B: Copy and create your own repo - Download the official code - Create a new Git repository - Push the code there Advantages: Cleaner history; feels like your own project from day one. We recommend GitHub Fork for long-term maintenance. 4. Install local tools Ensure you have: - Node.js 22 or newer - pnpm - Git Verify: node -v pnpm -v git --version 5. Clone and install dependencies git clone cd zship pnpm install 6. Log in to Wrangler npx wrangler login After authorization, verify: npx wrangler whoami 7. Do not change on first deployment - Workers: Keep names like zship-node1-auth, zship-node2-support, etc. - Frontend targets: Keep Worker web and Pages project admin - app_key: Use demo first; switch to your real key after the system works 8. Checklist Before continuing, confirm: - [ ] You have decided Fork vs. copy - [ ] Repository is cloned locally - [ ] pnpm install completed - [ ] npx wrangler login and npx wrangler whoami succeed - [ ] You will keep default service names on first pass ### CLI: Local Development URL: https://zship.ai/docs/cli-02-local-dev Set up local development environment with manual commands: install deps, migrate D1, start services. This chapter covers how to bring up the local development environment using the command line only. 1. Install dependencies From the repository root: pnpm install 2. Run local D1 migrations Each backend service that uses D1 needs its local database schema. Run migrations for each service: cd backend/node1-auth-service && npx wrangler d1 migrations apply zship-auth --local && cd ../.. cd backend/node2-support-service && npx wrangler d1 migrations apply zship-support --local && cd ../.. cd backend/node3-pay-service && npx wrangler d1 migrations apply zship-pay --local && cd ../.. cd backend/node4-notify-service && npx wrangler d1 migrations apply zship-node4-notify-db --local && cd ../.. cd backend/node5-blog-service && npx wrangler d1 migrations apply zship-blog --local && cd ../.. cd backend/node7-site-service && npx wrangler d1 migrations apply zship-site --local && cd ../.. cd backend/node8-prompt-service && npx wrangler d1 migrations apply zship-prompt --local && cd ../.. cd backend/node9-checkin-service && npx wrangler d1 migrations apply zship-checkin --local && cd ../.. cd backend/node10-ai-service && npx wrangler d1 migrations apply zship-ai --local && cd ../.. cd backend/node11-email-service && npx wrangler d1 migrations apply zship-email --local && cd ../.. Or use the per-service scripts if available: pnpm db:migrate:local pnpm db:migrate:support:local # ... repeat for each service with D1 3. Start backend Workers Open separate terminals for each backend service, or use a process manager. From the repository root: # Terminal 1 - Auth (port 8787) cd backend/node1-auth-service && npx wrangler dev # Terminal 2 - Support (port 8788) cd backend/node2-support-service && npx wrangler dev --port 8788 # Terminal 3 - Pay cd backend/node3-pay-service && npx wrangler dev --port 8789 # ... and so on for node4 through node11 Or use root scripts: pnpm dev:auth # node1-auth pnpm dev:support # node2-support # etc. 4. Start frontend apps In separate terminals: # Terminal - Web (port 4321) pnpm dev:web # Terminal - Admin (port 4322) pnpm dev:admin For apps/web-astro, create apps/web-astro/.env using the same variable names as production — e.g. AUTH_SERVICE_URL, PAY_SERVICE_URL, BLOG_API_URL (see Bind domains and environment variables for the usual set). The app reads service URLs via globalThis._importMeta_.env; zship.app.json owns app identity and presentation config, while src/config/site.ts adapts that manifest into typed siteConfig. 5. Verify - Web: http://localhost:4321 - Admin: http://localhost:4322 - Auth: http://localhost:8787 6. Checklist - [ ] Dependencies installed - [ ] All D1 migrations applied locally - [ ] Backend Workers running - [ ] Frontend apps running - [ ] Can access web and admin in browser ### CLI: Prepare Cloudflare Resources URL: https://zship.ai/docs/cli-03-cloudflare-resources Create Workers, D1 databases, KV namespace, and R2 bucket in Cloudflare using wrangler commands. Create the required Cloudflare resources for core delivery (10 D1, 1 KV, 1 R2) before deploying real code. Workers are created automatically when you run wrangler deploy — the name in each wrangler.jsonc defines the Worker, and bindings are applied on deploy. Value-added services (e.g. zship-provider1-service) are not included. 1. Create D1 databases From the repository root, run these commands. Each will create the database and update the service's wrangler.jsonc: cd backend/node1-auth-service && npx wrangler d1 create zship-auth --binding DB --update-config && cd ../.. cd backend/node2-support-service && npx wrangler d1 create zship-support --binding DB --update-config && cd ../.. cd backend/node3-pay-service && npx wrangler d1 create zship-pay --binding DB --update-config && cd ../.. cd backend/node4-notify-service && npx wrangler d1 create zship-node4-notify-db --binding DB --update-config && cd ../.. cd backend/node5-blog-service && npx wrangler d1 create zship-blog --binding DB --update-config && cd ../.. cd backend/node7-site-service && npx wrangler d1 create zship-site --binding DB --update-config && cd ../.. cd backend/node8-prompt-service && npx wrangler d1 create zship-prompt --binding DB --update-config && cd ../.. cd backend/node9-checkin-service && npx wrangler d1 create zship-checkin --binding DB --update-config && cd ../.. cd backend/node10-ai-service && npx wrangler d1 create zship-ai --binding DB --update-config && cd ../.. cd backend/node11-email-service && npx wrangler d1 create zship-email --binding DB --update-config && cd ../.. 2. Create KV namespace cd backend/node3-pay-service npx wrangler kv namespace create zship-pay-kv --binding KV --update-config cd ../.. 3. Create R2 bucket cd backend/node6-cdn-service npx wrangler r2 bucket create zship-cdn --binding BUCKET --update-config cd ../.. 4. Verify no placeholders remain rg -n "00000000-0000-0000-0000-000000000000|placeholder-replace-after-create" backend/*/wrangler.jsonc If this returns nothing, you are ready. If it finds matches, fix those configs before continuing. 5. Checklist - [ ] All 10 D1 databases created - [ ] KV namespace created - [ ] R2 bucket created - [ ] No placeholder IDs in wrangler.jsonc files ### CLI: Deploy Backend Workers URL: https://zship.ai/docs/cli-04-deploy-workers Deploy backend Workers using wrangler deploy. Fill secrets first, run remote migrations, then deploy in order. This step deploys only the 11 core Workers (node1-auth through node11-email). Value-added services (e.g. zship-provider1-service) are not included. 1. Fill required secrets first Before deploying, ensure node1-auth-service has its critical secrets filled. Check the service's wrangler.jsonc and use: npx wrangler secret put For each required secret. Run from the service directory, e.g.: cd backend/node1-auth-service npx wrangler secret put JWT_SECRET # ... other secrets as needed cd ../.. 1.5 How this maps to Dev Console “push env” wrangler secret put and vars in wrangler.jsonc + deploy both end up as Worker configuration in Cloudflare. If you use Dev Console → Deploy → Environment variables → Push to a Worker, you are updating the same remote Worker—just without terminal commands and often next to one-click deploy. Use either CLI or Dev Console for routine non-secret values; keep secrets in secret put or store them as Secrets in the UI—never commit them to git. 2. Run remote D1 migrations Apply migrations to Cloudflare D1 (not local): cd backend/node1-auth-service && npx wrangler d1 migrations apply zship-auth --remote && cd ../.. cd backend/node2-support-service && npx wrangler d1 migrations apply zship-support --remote && cd ../.. cd backend/node3-pay-service && npx wrangler d1 migrations apply zship-pay --remote && cd ../.. cd backend/node4-notify-service && npx wrangler d1 migrations apply zship-node4-notify-db --remote && cd ../.. cd backend/node5-blog-service && npx wrangler d1 migrations apply zship-blog --remote && cd ../.. cd backend/node7-site-service && npx wrangler d1 migrations apply zship-site --remote && cd ../.. cd backend/node8-prompt-service && npx wrangler d1 migrations apply zship-prompt --remote && cd ../.. cd backend/node9-checkin-service && npx wrangler d1 migrations apply zship-checkin --remote && cd ../.. cd backend/node10-ai-service && npx wrangler d1 migrations apply zship-ai --remote && cd ../.. cd backend/node11-email-service && npx wrangler d1 migrations apply zship-email --remote && cd ../.. 3. Deploy Workers in order Deploy each backend service. Recommended order (auth first, then others): cd backend/node1-auth-service && pnpm run deploy && cd ../.. cd backend/node2-support-service && pnpm run deploy && cd ../.. cd backend/node3-pay-service && pnpm run deploy && cd ../.. cd backend/node4-notify-service && pnpm run deploy && cd ../.. cd backend/node5-blog-service && pnpm run deploy && cd ../.. cd backend/node6-cdn-service && pnpm run deploy && cd ../.. cd backend/node7-site-service && pnpm run deploy && cd ../.. cd backend/node8-prompt-service && pnpm run deploy && cd ../.. cd backend/node9-checkin-service && pnpm run deploy && cd ../.. cd backend/node10-ai-service && pnpm run deploy && cd ../.. cd backend/node11-email-service && pnpm run deploy && cd ../.. Or use root scripts where available: pnpm deploy:auth pnpm deploy:support # etc. 4. If you see "binding target not found" - Ensure you did not rename any Worker (must match name in wrangler.jsonc) - Ensure you are logged into the correct Cloudflare account (npx wrangler whoami) - Deploy in dependency order (Workers without bindings first) 5. Checklist - [ ] Required auth secrets filled - [ ] Remote D1 migrations completed - [ ] All Workers deployed successfully - [ ] No binding-not-found errors ### CLI: Deploy Frontend Apps URL: https://zship.ai/docs/cli-05-deploy-pages Deploy apps/web-astro as a Cloudflare Worker and apps/admin as a Cloudflare Pages project. Deploy the core frontend apps only: apps/web-astro and apps/admin. 1. Confirm Cloudflare targets Use the current repo config as the source of truth: - apps/web-astro deploys as a Worker named web - apps/admin deploys as a Pages project named admin Keep these names unless you also update the matching wrangler.toml, domains, and environment variables. 2. Deploy apps/web-astro apps/web-astro uses Astro with @astrojs/cloudflare and deploys as a Worker with static assets. cd apps/web-astro pnpm run build npx wrangler deploy cd ../.. 3. Deploy apps/admin apps/admin uses Nuxt. Build and deploy to Cloudflare Pages: cd apps/admin npx nuxi build --preset=cloudflare_pages npx wrangler pages deploy dist --project-name=admin --branch=main --commit-dirty=true cd ../.. Or use the project's configured deploy command (e.g. pnpm run deploy if defined). 4. Set environment variables After deployment, configure frontend Cloudflare environment variables: - Backend service URLs (e.g. AUTH_SERVICE_URL, SUPPORT_SERVICE_URL, etc.) - ZSHIP_KEY if you use public HTTP fallback instead of Service Bindings - Any other required public vars For apps/web-astro, use Worker vars/secrets. For apps/admin, use Pages vars/secrets. 5. SESSION binding warning If you see a SESSION binding warning, the expected KV binding may not be configured. Check: - Wrangler/Pages binding configuration - Whether the target environment matches what you prepared 6. Checklist - [ ] apps/web-astro Worker deployed - [ ] apps/admin Pages project deployed - [ ] Critical environment variables set ### 00. Meet the Dev Console URL: https://zship.ai/docs/deploy-00-dev-console Before deployment, get familiar with the built-in ZShip console so you remember fewer commands, switch folders less often, and miss fewer steps. You are on Path B: Dev Console. You will use the browser UI for setup, migrations, service startup, and Cloudflare deployment instead of running commands manually. If you prefer the command line, see the CLI path. Before you start typing a long list of commands manually, take a moment to understand the built-in Dev Console that ships with ZShip. Its purpose is simple: - Help you memorize fewer commands - Help you switch directories less often - Help you miss fewer steps If this is your first time using the project, open the Dev Console before going into the deployment steps below. 1. What the Dev Console is The Dev Console is ZShip's built-in local control panel. After launch, you will see a local page in the browser that helps you manage: - Getting started - Local startup - Cloudflare deployment - Environment variables - Database migrations - Service status - Brand configuration - Documentation links In practice, it is a local operations panel. You do not need to memorize a large number of commands to understand what is or is not configured correctly. 2. How to start it Run this from the repository root: pnpm dev:console After startup, the browser should open: http://localhost:3900 If it does not open automatically, visit that URL manually. 3. What the Dev Console is good for If you are new to the project, these are the parts you will use most often. 1. Getting Started It walks you through: - Prerequisite checks - Dependency installation - Environment variable checks - Database migrations - Service startup - Environment verification This is especially useful when bringing up local development for the first time. 2. Local Startup In the UI you can see: - Backend service list - Frontend app list - Whether each service is currently running - The local port for each service - The logs for each service That is much easier than manually watching a dozen terminal windows. 3. Environment Variables You can inspect and edit these directly in the browser: - Backend wrangler.jsonc - Frontend .env This is extremely helpful for new users, because a common first blocker is simply not knowing which file they are supposed to edit. 4. Database Migrations The console can help you run local database migrations. That matters a lot during the first local setup. 5. Cloudflare Deployment Switch to the Deploy tab and click Run All. The console will automatically: - Create D1, KV, R2 resources - Run database migrations - Deploy the 11 core Workers to Cloudflare - Deploy the core frontend apps (apps/web-astro and apps/admin) Fully automated for the core delivery path. Optional apps and value-added Workers remain available as individual Deploy cards. See 02. Deploy to Cloudflare for details. 4. Dev Console vs Cloudflare Dashboard Situation Prefer One place for checks, migrations, deploys, editing Worker env vars, then Push Dev Console (Deploy to Cloudflare and env tooling) Routine updates to CDN_PUBLIC_URL / cdn backend vars plus redeploy Dev Console → Shipping → Deploy → Environment variables (see Troubleshooting §3) Production error stacks, request traces, console output for Workers / Pages Cloudflare Dashboard → Observability (Troubleshooting §2) First-time R2 Custom Domains and bucket settings in the web UI Cloudflare Dashboard → R2 (Troubleshooting §3, Path A) Rule of thumb: stay in Dev Console whenever you can; open the Dashboard mainly for Observability, R2 hostname setup, and any workflow you prefer to click through in the CF UI. 5. When to use the Dev Console (day to day) Use the Dev Console when: - You are starting the local environment for the first time - You want to deploy to Cloudflare (one-click) - You want to confirm whether services started correctly - You want quick access to a service's logs - You are unsure which environment variables need to be changed 6. What you should do now If you have never opened the console before, run this now: pnpm dev:console Then switch to the Deploy tab and click Run All when you are ready to deploy to Cloudflare. ### 01. Before You Start URL: https://zship.ai/docs/deploy-01-before-start Preparation before deployment: accounts, source access strategy, local tools, and Wrangler login. Do not skip this chapter. If you do not prepare this part properly, you will almost certainly get stuck later. 1. What you are actually deploying ZShip is not a single-page project. It is a full system that you eventually deliver to users, including at least: - 11 backend Workers - 2 frontend apps that users will interact with - 10 D1 databases - 1 KV namespace used by the payment service - 1 R2 bucket used by the CDN service So you are not deploying just one website. You are deploying a Cloudflare-based system made of frontend apps, an admin backend, microservices, databases, and storage. 2. What accounts you need At minimum, prepare: - A Cloudflare account - A copy of the Git repository - A machine that can run Node.js Optional accounts: - A Stripe account if you want real payments - A Creem account if you want Creem payments - A Dodo Payments account if you want Dodo payments - A Waffo Pancake account if you want Waffo payments - A Google OAuth app if you want Google login 3. Decide how you will obtain the source code This is one of the easiest choices to overlook at the start, but it directly affects long-term maintenance cost. You have two options. Option A: GitHub Fork How it works: - Open the official template repository - Click the Fork button in the top-right corner of GitHub - Fork the repository into your own account or organization - Clone your forked repository locally Advantages: - It preserves the relationship to the official template repo - Future upstream updates are easier to sync manually - It is the best fit if you want to maintain your product on top of the template for a long time Disadvantages: - It will not update automatically - Future upstream updates still require manual handling Option B: Copy the code and create your own repo How it works: - Download the official template code - Remove the original Git history or do not keep the original repo relationship - Create a brand-new repository under your own GitHub account - Push the code there Advantages: - The repository history feels more like your own standalone project - Some teams prefer this setup psychologically and operationally Disadvantages: - You do not keep a ready-made upstream sync path - Following future official changes becomes manual comparison and merging work Which one we recommend Recommended: - Choose GitHub Fork Why: - The official template will continue to receive updates - A fork preserves your path for adopting those updates later - For most users, this results in the lowest long-term maintenance cost But remember: - Fork does not automatically give you future template updates When the official template changes: - You still need to sync upstream manually - On GitHub you can use Sync fork - Or do it locally with fetch plus merge or rebase 4. What you need installed locally Make sure your machine already has: - Node.js 22 or newer - pnpm - Git After installation, run: node -v pnpm -v git --version All three commands should return valid versions before you continue. 5. Start the Dev Console first If this is your first time touching the project, run this before going further: pnpm dev:console Then visit: http://localhost:3900 You do not need to click every feature yet. Just make sure you can see these three tabs: - Getting Started - Local Startup - Deploy Cloudflare That will reduce your later debugging cost dramatically. 6. Pull the repository locally If you already have the repo, you can skip this section. If not, clone your repository: git clone cd zship If you chose Fork The repository URL here should be your own forked repository. It should not be the official repository URL. If you chose "create a new repo yourself" Then this URL should be your own brand-new repository. After that, install dependencies: pnpm install Installation may take a while. That is normal. If pnpm install fails, do not move on to later steps until dependencies are fixed. 7. Log in to Wrangler Wrangler is Cloudflare's official command-line tool. This project uses it to create and deploy Workers, Pages, D1, KV, and R2 resources. Run this from the repository root: npx wrangler login Your browser will open a Cloudflare authorization page. After authorization completes, run: npx wrangler whoami If you can see your Cloudflare account information, Wrangler login is working. 8. What not to change on the first deployment Do not rename these Workers - zship-node1-auth - zship-node2-support - zship-node3-pay - zship-node4-notify - zship-node5-blog - zship-node6-cdn - zship-node7-site - zship-node8-prompt - zship-node9-checkin - zship-node10-ai Do not rename these frontend targets - Worker web - Pages project admin 9. Checklist before you leave this chapter Confirm that: - You have decided whether to use Fork or create your own repository - You know how to start pnpm dev:console - You have the repository cloned locally - You have run pnpm install - You have run npx wrangler login - You have run npx wrangler whoami - You understand that the first pass should keep the default service names If any item above is still unfinished, do not continue to the next chapter yet. ### 02. Deploy to Cloudflare URL: https://zship.ai/docs/deploy-02-cloudflare-resources One-click deployment: Dev Console creates D1/KV/R2, runs migrations, deploys Workers and frontend. Path B (Dev Console) — Fully automated. If you prefer CLI, see Path A. 1. Open Dev Console → Deploy tab - Run pnpm dev:console from the repo root. - In the Dev Console menu, switch to Deploy (云端部署). - You will see the deployment pipeline with steps 0–6. 2. Click Run All Click Run All (执行全部步骤). The pipeline will automatically: - Step 0 — pnpm install - Step 1 — Create D1, KV, R2 resources - Step 2 — Run D1 database migrations - Step 3 — Deploy all 11 core Workers to Cloudflare - Step 5 — Deploy the core frontend apps (apps/web-astro and apps/admin) No manual creation. No separate commands for the core delivery path. Optional apps and value-added Workers can be deployed from their own cards. 3. Fill secrets if needed Before or after the first run, fill auth secrets in the Environment / Secrets section (e.g. JWT_SECRET, ADMIN_SECRET for node1-auth). Secret keys are not shipped in vars; production values must be set as secrets (the section pushes them via wrangler secret put), while local development reads placeholders from each service's .dev.vars (see .dev.vars.example). ADMIN_SECRET is the key for accessing the admin panel. 4. Next steps Once deployment completes, proceed to 05. Domains & Environment and 06. Initialize System. ### 03. Deploy Backend Workers URL: https://zship.ai/docs/deploy-03-backend-workers Use Dev Console to fill secrets, run remote migrations, and deploy the 11 backend Workers. Path B (Dev Console) — This chapter is for the Dev Console workflow. If you prefer CLI commands, see Path A: Deploy Backend Workers. Deploy the 11 core Workers (node1-auth through node11-email) only. Value-added services (e.g. zship-provider1-service) are not included. 1. Fill required secrets first In Dev Console → Deploy tab → Environment / Secrets section: - Fill the auth-related secrets for node1-auth-service (required for login and initialization). - Fill any other mandatory backend startup values. - Optional: payment provider keys (Stripe, Creem, Dodo, Waffo) and AI provider keys can be added later if your launch does not depend on them immediately. 2. Run remote migrations via Dev Console Before deploying code, run remote database migrations: - In Dev Console → Deploy tab → Migration section. - Click Migrate All (or migrate each backend one by one). - Wait for all migrations to complete. The Workers expect schemas and tables to exist; deploying before migration often causes confusing runtime failures. 3. Deploy backend Workers via pipeline In Dev Console → Deploy tab → Pipeline: - Ensure steps 0 (pnpm install) and 1 (Provision Resources) are done from the previous chapters. - Step 2 (Remote Migration) should be done from the previous section. - Click Run All to execute the pipeline, or run step 3 (Deploy Backend) only. - The Dev Console will deploy each Worker in sequence. Watch the console log for progress and errors. Alternative: deploy individual Workers You can also deploy Workers one by one from the canvas cards. Click the Deploy button on each backend service card. 4. If you see "binding target not found" Common causes: - Renamed Workers — If you changed default Worker names, bindings will not match. - Deploy order — Workers are created on first deploy. If a service binding fails, the target Worker may not exist yet; try running Run All so the pipeline deploys in dependency order. - Wrong account — Resources created in account A but Wrangler logged into account B. 5. Checklist before you leave this chapter - [ ] Required auth secrets are filled - [ ] Optional payment/AI secrets filled if needed - [ ] Remote migrations have run successfully - [ ] All 11 core Workers deployed successfully - [ ] No binding-not-found errors blocking startup Once all of that is stable, you are ready for the frontend. ### 04. Deploy Frontend Apps URL: https://zship.ai/docs/deploy-04-frontend-pages Use Dev Console to deploy apps/web-astro as a Worker and apps/admin as a Pages project. Path B (Dev Console) — This chapter is for the Dev Console workflow. If you prefer CLI commands, see Path A: Deploy Frontend Apps. Deploy the core frontend apps only: apps/web-astro and apps/admin. The Dev Console follows each app's config: apps/web-astro deploys to the web Worker, and apps/admin deploys to the admin Pages project. 1. Deploy via Dev Console pipeline In Dev Console → Deploy tab → Pipeline: - Ensure backend deployment (step 3) is complete. - Run step 5 (Deploy Frontend). The Dev Console will build and deploy both web and admin in sequence. - Or use the canvas: click the Build & Deploy button on each frontend app card (web, admin). 2. Configure frontend environment variables After the first deployment: - In Dev Console → Deploy tab → Frontend Cloud Env or Environment section. - Set apps/web-astro to point to: - Correct backend service URLs - ZSHIP_KEY if you use public HTTP fallback instead of Service Bindings - Any required public environment values for login or site configuration - Set apps/admin to point to auth-related URLs and service endpoints. Use repository defaults first, verify the system works, then customize. 3. If you see a SESSION binding warning A warning about an invalid SESSION binding usually means the Cloudflare KV binding is not configured yet: - Check the Wrangler binding configuration - Check Cloudflare Pages or Worker environment bindings - Ensure the target environment matches the one you prepared earlier 4. Checklist before you leave this chapter - [ ] apps/web-astro Worker deployed successfully - [ ] apps/admin Pages project deployed successfully - [ ] Critical frontend environment variables are filled - [ ] SESSION warning understood and resolved if it was real Once those are done, you can move on to domains and production URLs. ### 05. Bind Domains and Environment Variables URL: https://zship.ai/docs/deploy-05-domain-env Bind production domains, switch frontend variables to real online addresses, and understand how copied web apps should be wired. This chapter is where your deployment starts looking like a real online product instead of a set of temporary project URLs. 0. Why bind domains? The system may run without binding custom domains. But: - URLs will be ugly: default *.workers.dev or *.pages.dev URLs are hard to remember and share. - Payment callbacks need public URLs: Stripe, Creem, Dodo, and Waffo must reach your backend from the public internet. - SEO metadata should not point at preview domains: sitemap, canonical tags, and robots should use your real production domain. node3-pay-service must be bound first because it receives payment provider callbacks. Without a custom domain, Stripe, Creem, Dodo, and Waffo webhooks are usually unreliable. 1. Bind custom domains Web (Astro 6) web currently deploys to a Worker, not Pages. Bind your custom domain in Cloudflare Dashboard -> Workers & Pages -> your web Worker -> Settings -> Domains & Routes. Recommended workflow: Copy apps/web-astro to a new project and customize there. Do not modify apps/web-astro directly. Treat it as the official template. If you copy apps/web-astro to create a new site Use this order in the current system: - Create the tenant in Admin first - Open Admin -> Projects - Create a new project row with a unique app_key - This registers the tenant in t_project - Copy apps/web-astro to a new app directory - Example: apps/my-brand - Edit apps/my-brand/zship.app.json - appKey: must match the tenant you created in Admin - domain: your production domain - siteUrl: your canonical production URL - pagesProject: the deployment target name for this frontend app - Brand fields such as siteName, brand.name, and brand.logo - Keep runtime wiring aligned - zship.app.json owns appKey, domain, and siteUrl - Env variables are for backend service URLs, secrets, and legacy/env-based overrides - Change wrangler.toml - Update the name field - If you keep name = "web" in the copied app, deployment may overwrite the original web Worker - Bind the new custom domain - Give the copied frontend its own domain - Do not reuse the original web domain unless you intentionally want to replace it - Redeploy the copied frontend - Treat it as its own frontend Worker and deploy it independently Mental model: - Admin creates the tenant - zship.app.json maps a frontend app to that tenant - wrangler.toml decides which Worker receives the deployment Admin admin usually deploys to Pages. Bind the domain in Workers & Pages -> admin -> Custom domains. Backend Workers Priority: node3-pay-service. Bind it first so Stripe, Creem, Dodo, and Waffo webhooks can reach your payment callback endpoint. Other Workers can follow. CDN and R2 (CDN_PUBLIC_URL) If you use node6-cdn-service for uploads, the Worker's CDN_PUBLIC_URL must match the R2 bucket custom domain. Otherwise uploads may succeed while public URLs return 404. Full steps are in Troubleshooting. 2. How zship.app.json, site.ts, and env work together Frontend configuration is layered: - zship.app.json - Recommended source of truth for frontend app identity - Manages appKey, siteUrl, domain, brand metadata, and deployment target names - site.ts - Runtime adapter that reads zship.app.json and turns it into typed siteConfig - Regenerated by Dev Console only when scaffold/brand tooling needs to refresh the adapter - Environment variables - Local: .env - Production: Cloudflare Variables and Secrets - Service URLs and secrets come from env; app identity and canonical URL come from the manifest unless a legacy app explicitly enables env-based tenant selection So the practical rule today is: - Edit zship.app.json - Let Dev Console keep robots and generated adapters aligned when you use its tooling - Configure env for backend URLs and secrets only 3. Canonical URL vs latest deployed URL Do not treat these as the same thing: - Canonical URL: your real production site URL, for example https://my-brand.com - Latest deployed URL: the latest preview or temporary URL discovered during deploy, for example https://xxxx.my-brand.pages.dev Current rule: - siteUrl in zship.app.json is the canonical URL - the latest deployed pages.dev URL is only a deploy artifact and should not replace sitemap, robots, or canonical metadata 4. Recommended frontend variables For a copied web project, put identity in zship.app.json and configure service endpoints with env values like: AUTH_SERVICE_URL=https://n1.example.com PAY_SERVICE_URL=https://n3.example.com BLOG_API_URL=https://n5.example.com SITE_SERVICE_URL=https://n7.example.com CHECKIN_SERVICE_URL=https://n9.example.com AI_SERVICE_URL=https://n10.example.com SUPPORT_SERVICE_URL=https://n2.example.com Important: - appKey in zship.app.json must match the tenant created in Admin - siteUrl in zship.app.json should be the real production URL, not a temporary pages.dev preview URL 5. Redeploy after env or domain changes After changing env, custom domains, or frontend identity, redeploy the frontend. Otherwise old values may still be baked into the build output. 6. Final sanity check Before initialization, make sure: - The public site opens under the correct production domain - Admin opens under the correct production domain - Login and registration target the correct tenant - Frontend requests hit the real production backend URLs - Sitemap, robots, and canonical metadata point at the production domain rather than pages.dev ### 06. Initialize Your System URL: https://zship.ai/docs/deploy-06-initialization Log in to the admin, create the first project, and finish the core configuration for payments, AI, and site content. At this point, the system is online. Now you need to make it usable. 0. Keep the Dev Console open during this step This is a good phase to keep the Dev Console available for reference. It helps when you need to double-check: - app keys - service values - environment files - startup status 1. Log in to the admin first Your first real operational step is logging into the admin interface. If you forgot what values you entered Check the environment variables and auth-related config first before assuming the system is broken. Most "I cannot log in" problems at this stage are configuration mismatches, not deep code bugs. If login keeps returning 401 Treat that as a configuration problem first. Check: - auth service secrets - frontend auth endpoint configuration - production domain settings - whether the frontend is still pointing at old or local addresses Operators & RBAC (optional) After the console works with ADMIN_SECRET, you can create operator accounts (email + password) with scoped permissions instead of sharing the root secret—for example read-only access to dashboards and logs for operational visibility without broad edit/delete rights (revoke or deactivate the account when access should end). See Admin RBAC & operator accounts, section Use case: let people observe operations without mutating data. 2. Create a demo project first For the first pass, create one project with the default key. Recommended first values Start with something simple and predictable: - use demo as the first app_key - fill the minimum required project fields - avoid customizing too many product-specific settings until the base flow works This is the fastest path to a fully verified system. 3. If you need payments, continue with payment channels After the basic project exists, configure payment channels if your product needs them. That typically includes: - enabling providers - filling product or price data - confirming callback and webhook behavior 4. If you need AI, continue with model setup If your product depends on AI features, this is the right time to configure: - model providers - API keys - gateway settings - model mappings 5. If you want the site to feel like a real product, fill these sections Do not stop at infrastructure. Use the admin to fill the parts that make the product look complete: - site branding - visible product copy - pricing content - support and contact information - blog or content settings if needed 6. Check auxiliary modules such as check-in, support, and notifications After core setup, verify the helper modules too. These modules are easy to ignore during first deploys, but they matter for real use: - check-in - support - notifications 7. If you are ready to switch to your own app_key Once the system is stable with demo, you can move to your real production app_key. Do it carefully: - create or update the project in admin - switch the frontend environment variables - redeploy the frontend - verify the site is now reading the intended project data 8. Checklist before you leave this chapter Before calling the initialization complete, confirm: - you can log into the admin - a demo project has been created successfully - payments are configured if needed - AI is configured if needed - site branding and core content are filled - support, notifications, and other helper modules are behaving normally - the real app_key migration plan is clear if you intend to switch away from demo 9. Cross-check with launch troubleshooting After initialization, walk through Launch Checks §1 again (public site, admin, signup/login, optional modules). If the admin Health page shows a service in a bad state, follow its hints and use §2 Observability on the matching Worker. Multiple environments: these docs assume one primary production stack. If you add staging or preview, use separate app_key values, Workers/Pages projects, or env vars so webhooks and domains are not shared with production by mistake. ### 07. Launch Checks and Common Errors URL: https://zship.ai/docs/deploy-07-troubleshooting Go-live checklist and fixes: Observability, CDN/R2, email & webhooks, bindings, D1/KV/R2, auth, payments, AI. This chapter is for the final check before you call the system live, plus the most common deployment mistakes people hit on their first pass. 1. Launch checklist Before you call the deployment done, verify the basics one more time. Frontend access Check: - the public site opens normally - the admin opens normally - the correct domains are being used Admin access Check: - login works - the admin can read real project data - obvious configuration pages are usable Core user flow Check: - signup or login works - critical pages are not empty - the product can read the current app_key data correctly Optional modules If you enabled them, also check: - payment flow - AI flow - blog flow - notifications - check-in 2. Production debugging: Cloudflare Observability (error logs) When something fails in production and you cannot reproduce it locally, use the Cloudflare dashboard to inspect live Worker / Pages telemetry and errors. Navigation (example: auth Worker): - Open the Cloudflare Dashboard - Go to Compute → Workers & Pages - Open the target project, e.g. zship-node1-auth (use the exact name from your wrangler config) - Open Observability From Observability you can inspect requests, error stacks, console output, and related signals—useful for 5xx responses, uncaught exceptions, and upstream failures. If logs are empty: Observability may not be enabled for that Worker or Pages project yet. In the same project’s settings, find the Observability controls and enable them, then reproduce the issue and refresh the view. Tip: Repeat for each service you need to debug (for example zship-node3-pay, zship-node10-ai, or a frontend Worker / Pages app)—each has its own Observability scope. 3. CDN / R2 issues: keep CDN_PUBLIC_URL aligned with the R2 public hostname When uploads fail, files 404, or node6-cdn-service errors, first verify that the public base URL in the Worker matches the R2 custom domain you serve files from. If they diverge, the Worker may write objects correctly but return URLs that do not resolve to the same public entrypoint. Two tracks below: §3.1–§3.4 are entirely in the Cloudflare Dashboard (first-time domain setup and UI walkthrough). §3.5 uses the ZShip Dev Console — for day-to-day CDN_PUBLIC_URL updates, prefer Dev Console so you skip hunting through Workers & Pages → Variables and Secrets in the CF UI. Path A — Cloudflare Dashboard (§3.1–§3.4) Everything in this path happens in the Cloudflare Dashboard: R2 custom domains, Worker env vars, and optional Worker hostnames. 3.1 Set CDN_PUBLIC_URL in the Dashboard - Compute → Workers & Pages → open zship-node6-cdn - Settings → Variables and Secrets - Set CDN_PUBLIC_URL to your R2 public base URL (include https://, usually no trailing path) 3.2 Where the R2 public URL comes from - Storage & databases → R2 object storage - Under Overview, open your bucket (example name zship-cdn — use the bucket you actually created) - Settings → Custom Domains Bind the hostname that should serve files publicly (for example https://cdn.example.com). 3.3 The two values must match Location Setting What to put Worker zship-node6-cdn CDN_PUBLIC_URL Same root URL as the R2 custom domain (https://your-cdn-hostname) R2 bucket Custom Domains The same hostname you configured above They must stay in sync so zship-node6-cdn generates URLs that point at the same objects users can fetch. 3.4 Exposing the CDN Worker on the public internet If you also need the upload Worker itself on its own hostname (not only the R2 public domain), bind a domain / route to zship-node6-cdn under Workers & Pages → that Worker → Settings → Domains & Routes (or Custom Domains). Many setups only need §3.3—R2 custom domain + matching CDN_PUBLIC_URL. Path B — ZShip Dev Console (§3.5) — preferred for routine changes Why Dev Console matters: You can edit CDN_PUBLIC_URL, push straight to the live Worker, and follow with a redeploy in one workflow — without bouncing between Cloudflare menus (Workers & Pages → … → Variables and Secrets). It matches how you already manage secrets and deploys from this repo, which cuts context switching when debugging or shipping. 3.5 Push CDN_PUBLIC_URL from Dev Console and redeploy - Open Dev Console → Shipping → Deploy (labels may match your local build) - Environment variables → Worker environment - Select cdn backend (maps to zship-node6-cdn) - Edit CDN_PUBLIC_URL, then Push to zship-node6-cdn (or equivalent) to write vars to the live Worker - Redeploy zship-node6-cdn so the change takes effect Note: If the R2 Custom Domain has never been created, do §3.2 in the Dashboard once. After that, routine URL-only updates can stay in §3.5. 4. Common error 1: service binding target not found Typical causes - you renamed a Worker (does not match name in wrangler.jsonc) - you deployed using the wrong Cloudflare account - deploy order: a Worker with service bindings depends on a target that has not been deployed yet What to do - keep Worker names consistent with wrangler.jsonc - run wrangler whoami and verify you are in the intended account - deploy in dependency order (Workers without bindings first) 5. Common error 2: D1, KV, or R2 resources cannot be found Typical causes - resources were never created - config files still contain placeholders - resource IDs were copied incorrectly What to do - verify the resources exist in Cloudflare - compare config values carefully - replace all placeholder values before redeploying 6. Common error 3: admin login returns 401 Typical causes - auth secrets are wrong - frontend points at the wrong auth URL - the domain configuration is inconsistent What to do - verify auth secrets again - verify auth endpoints again - confirm the deployed frontend is using the latest environment values 7. Common error 4: pages open, but all data is empty Typical causes - the frontend points at the wrong backend - the wrong app_key is in use - the admin and frontend are not reading the same dataset What to do - verify frontend environment variables - verify the real app_key - confirm the backend services and admin data belong to the same project 8. Common error 5: the payment page opens, but payment does not work Typical causes - provider keys are missing - webhook config is incomplete - pricing or product data is not configured correctly What to do - verify payment provider secrets - verify webhook configuration - verify product and pricing data in the admin 9. Common error 6: the AI page opens, but sending a message fails Typical causes - provider keys are missing - model config is incomplete - gateway or upstream settings are wrong What to do - verify AI provider keys - verify model mapping - verify the deployed AI service is reading the intended environment values 10. Common error 7: Cloudflare deploy says Invalid binding SESSION Cause This usually means the expected binding exists in code or config, but is not properly created or attached in the target environment. What to do - verify the Cloudflare binding exists - verify the Worker or Pages environment is using the right binding name - redeploy after fixing the binding configuration 11. Email does not send or lands in spam Typical causes - DNS for your sending domain is incomplete (SPF, DKIM, DMARC — follow your mail provider’s checklist) - Wrong SMTP / provider secrets or sender settings for node4-notify-service (admin or Worker env) - Broken templates or routing so requests never reach the notify Worker What to do - Follow Email for DNS and channel configuration - Check Observability on zship-node4-notify (or your renamed notify Worker) for errors - Send a test message from the admin and review provider delivery logs if available 12. Payment or third-party webhooks never arrive / status stuck Typical causes - Callback URL is not a public hostname (still on *.workers.dev or internal URL) — same as Domains & environment: node3-pay needs a real domain - Stripe / Creem / Dodo / Waffo webhook URL or signing secret does not match production (wrong environment, mixed staging/prod) - Provider retries expired — you may need to reconcile manually in admin What to do - Confirm the zship-node3-pay public URL matches the webhook endpoint in the payment dashboard (including https://) - Use Observability on zship-node3-pay to see whether callbacks hit the Worker and whether they return 4xx/5xx - Re-read Payment for price, channel, and project bindings If the browser shows CORS errors, the frontend origin often does not match what the API allows — align manifest siteUrl, project domains, and gateway settings, then retry. 13. One final recommendation If something breaks, do not panic and do not rewrite half the setup at once. Use the simplest debugging loop: - identify one likely cause - change one thing - redeploy or retry - observe the result That is the fastest way to converge on a stable production setup. ### Deployment Guide URL: https://zship.ai/docs/deployment-guide Deploy ZShip core: apps/web-astro, apps/admin, and 11 backend Workers. This guide covers deployment of the core delivery only: - Frontend: apps/web-astro (user site), apps/admin (admin console) - Backend: node1-auth ~ node11-email (11 Workers) Value-added services (e.g. AI Provider Service) are not included. See Value-Added Services. Choose your path In the documentation sidebar, every step for both tracks is listed under Deployment Guide. Pick one track below and follow only those pages in order. ZShip supports two ways to set up local development and deploy to Cloudflare: CLI (manual commands) Use this path if: - You are on a base/minimal setup without Dev Console - You prefer the command line or need to script deployments (CI, automation) - You want full control over each step You will run pnpm install, wrangler d1 create, wrangler deploy, and similar commands manually in your terminal. Start here: CLI: Before You Start Dev Console (UI) Use this path if: - You have the full ZShip template with Dev Console - You prefer a graphical interface for local startup and deployment - You want fewer commands to remember and fewer steps to miss You will run pnpm dev:console once, then use the browser UI for setup, migrations, service startup, and Cloudflare deployment. Start here: Meet the Dev Console Please read your chosen path in order the first time through. Two ways to get the source code When users receive this template, there are usually two approaches. Option 1: GitHub Fork This means: - The user directly forks the official repository into their own GitHub account or organization. Advantages: - It keeps the upstream relationship with the official repository. - It is easier to manually sync future template updates. - It fits the common workflow where the official template keeps evolving and users build their own product on top of it. Disadvantages: - It does not update automatically. - Users still need to sync upstream changes by hand. - If a user changes a lot locally, future syncs can create conflicts. Option 2: Copy the code and create a new repository This means: - The user downloads the official codebase. - They then create a brand-new Git repository under their own account. Advantages: - The repository history looks cleaner. - Some teams prefer the feeling that this is fully their own project from day one. Disadvantages: - There is no upstream relationship to the official template. - Future official updates will not have a ready-made sync path. - Keeping up with template improvements usually means manual compare-and-copy work. Recommended approach We recommend users start with: - GitHub Fork Why: - The official template will continue to evolve. - A fork at least preserves a clean path for manually syncing future official changes. - For most users, long-term maintenance cost is lower than copying the repo into a totally separate history. But this point must be crystal clear: - Fork does not mean you automatically receive official updates. In practice that means: - The official template can keep moving. - Users will not receive those changes automatically. - Users must sync upstream manually. What this guide covers This guide covers the Cloudflare deployment path for: - backend/node1-auth-service - backend/node2-support-service - backend/node3-pay-service - backend/node4-notify-service - backend/node5-blog-service - backend/node6-cdn-service - backend/node7-site-service - backend/node8-prompt-service - backend/node9-checkin-service - backend/node10-ai-service - backend/node11-email-service - apps/web-astro - apps/admin Reading order by track If you use the CLI - CLI: Before You Start - CLI: Local Development - CLI: Prepare Cloudflare Resources - CLI: Deploy Backend Workers - CLI: Deploy Frontend Apps - Bind Domains and Environment Variables - Initialize Your System - Launch Checks and Common Errors If you use Dev Console - Meet the Dev Console - Before You Start - Prepare Cloudflare Resources - Deploy Backend Workers - Deploy Frontend Apps - Bind Domains and Environment Variables - Initialize Your System - Launch Checks and Common Errors Troubleshooting and ops index Topic Doc Launch checklist, Observability, CDN/R2, email & webhooks, common errors Launch Checks Dev Console vs Cloudflare Dashboard Meet the Dev Console §4 Secrets and repo safety Important Notes — Security ### GitHub OAuth Setup URL: https://zship.ai/docs/github-oauth-setup How to configure GitHub OAuth login for your ZShip project. Note: GitHub OAuth setup is now part of Integrations → Authentication. The content below is kept for reference. Go to Authentication This guide covers how to enable "Continue with GitHub" login for your ZShip project. Prerequisites Before you begin, make sure: - Your ZShip system is deployed and running - You have access to the admin panel - You have a GitHub account with permission to create OAuth Apps 1. Create a GitHub OAuth App Go to GitHub and create a new OAuth App. For a personal account - Go to GitHub Developer Settings → OAuth Apps - Click New OAuth App For an organization - Go to https://github.com/organizations/{your-org}/settings/applications - Click New OAuth App Fill in the required fields Field Value Application name Your app name (e.g. ZShip) Homepage URL Your app URL (e.g. https://app.zship.ai) Authorization callback URL https://{your-domain}/auth/callback Replace {your-domain} with your actual domain. For example: - https://app.zship.ai/auth/callback - https://zship.ai/auth/callback Get your credentials After creating the OAuth App: - Copy the Client ID — you will need it in the next step - Click Generate a new client secret - Copy the Client Secret immediately — GitHub will only show it once 2. Configure in Admin Panel - Log in to the admin panel - Go to Projects and select (or edit) your project - Expand the OAuth Providers section - Find the GitHub card: - Toggle the switch to Enabled - Paste the Client ID from step 1 - Paste the Client Secret from step 1 - Click Save Once saved, the "Continue with GitHub" button will automatically appear on the login page. 3. Verify the integration - Open your app's login page - You should see a Continue with GitHub button below the Google sign-in button (if configured) - Click it — you should be redirected to GitHub's authorization page - After authorizing, you should be redirected back and logged in How it works The GitHub OAuth flow uses the standard Authorization Code grant: - User clicks "Continue with GitHub" on the login page - Browser redirects to GitHub's authorization page with user:email scope - User authorizes the app on GitHub - GitHub redirects back to /auth/callback with an authorization code - The server exchanges the code for an access token (using the client secret) - The server fetches the user's GitHub profile and verified email - The user is logged in (or auto-registered) and redirected to the dashboard Multiple callback URLs If you deploy more than one frontend on different hostnames (for example marketing vs app subdomains), each hostname needs its own callback URL. GitHub OAuth Apps only support one callback URL. You have two options: Option A: Create two OAuth Apps Create separate OAuth Apps for each frontend, each with its own callback URL: - https://app.zship.ai/auth/callback - https://zship.ai/auth/callback Then configure each project in admin with the corresponding client ID and secret. Option B: Use one OAuth App with a shared domain If both apps share the same domain (e.g. via path-based routing), you only need one OAuth App. Troubleshooting "GitHub sign-in is not configured" The GitHub provider is not enabled or the client ID is missing in the admin panel. Go to Admin → Projects → OAuth Providers and check the GitHub configuration. "Unable to retrieve email from GitHub" The GitHub account does not have a verified email, or the email is set to private without the user:email scope. Make sure the OAuth App requests the user:email scope (this is handled automatically by ZShip). Redirect URI mismatch The callback URL configured in GitHub must exactly match https://{your-domain}/auth/callback. Check for trailing slashes, protocol (must be https), and domain mismatches. ### AI URL: https://zship.ai/docs/integrations/ai AI gateway, multi-model chat, sessions. Backend: node10-ai-service. The AI gateway is handled by node10-ai-service. It provides multi-model chat, session history, and credit-based billing. Core AI (node10-ai) - Uses Cloudflare Workers AI — free models deployed on Cloudflare - Manages model config, credit costs, rate limits - Chat sessions, conversation history Admin pages - AI (/ai) — Model config, gateway settings, credit costs Image Agent The official Nuxt app includes a conversational image workspace. See Image Agent for the product behavior, Admin setup, and the BFF your own UI should call. Value-added: AI Provider Service For extended AI capabilities (e.g. KIE, WaveSpeed AI image/video models), see AI Provider Service — a separate purchasable service. Image Agent sends its image calls through that service. ### Authentication URL: https://zship.ai/docs/integrations/authentication User auth, OAuth (Google, GitHub), API keys. Backend: node1-auth-service. Authentication is handled by node1-auth-service. It supports email/password, Google OAuth, GitHub OAuth, guest login, and API keys. Email & password Configured via Admin. Users register and log in with email via verification code or magic link. Google OAuth - Create a Google OAuth client in Google Cloud Console - In Google Console, add your frontend domain to Authorized JavaScript origins (e.g. https://your-domain.com) — Google uses popup mode, no redirect callback needed - Log in to Admin → Projects → select your project → OAuth Providers - Enable Google, paste the Client ID, save GitHub OAuth Enable "Continue with GitHub" login: 1. Create a GitHub OAuth App - Go to GitHub Developer Settings → OAuth Apps - Click New OAuth App - Fill in: - Application name: Your app name - Homepage URL: Your app URL (e.g. https://app.zship.ai) - Authorization callback URL: https://{your-domain}/auth/callback - Copy Client ID and Client Secret 2. Configure in Admin Panel - Log in to admin - Go to Projects → select your project → OAuth Providers - Enable GitHub, paste Client ID and Client Secret - Save The "Continue with GitHub" button will appear on the login page. API keys Users can create API keys in the dashboard. Keys are stored in node1-auth and used for programmatic access (e.g. AI gateway). Guest mode Optional guest (anonymous) sessions for trying the product before full registration. Admin pages - Users (/users) — User list, credit adjustments - Credits (/credits) — Credit management ### Blog URL: https://zship.ai/docs/integrations/blog Blog CMS, posts, public API. Backend: node5-blog-service. Blog content is handled by node5-blog-service. Markdown-based CMS with public API. Why ship a built-in blog CMS - Organic discovery (SEO) — Long-form posts become indexable pages on your domain. That helps you rank for product keywords, tutorials, and comparisons without bolting on a separate WordPress site. - Trust and narrative — Changelog-style updates, how-tos, and case notes show that the product is alive. That supports conversion on Pricing and reduces “is this maintained?” friction. - Publish without redeploying the app — Editors use the admin Blog UI; you are not editing marketing repo files or running a deploy for every announcement. Content stays in one pipeline with the rest of ZShip. - Headless-friendly API — GET /api/blog/list and GET /api/blog/get let apps/web-astro (Astro), landing, or a future front-end render posts consistently—same data, multiple surfaces if you need them. - Fits the stack — Same auth and tenant model as the rest of the platform; Markdown plus CDN-friendly images (node6 or external URLs) keeps hosting simple on Cloudflare. Together, the blog turns your site from a static brochure into a content channel that compounds: more pages, more reasons to return, and clearer positioning for AI/SaaS buyers. Admin pages - Blog (/blog) — Create, edit, publish posts Public API - List posts: GET /api/blog/list - Get post: GET /api/blog/get?slug=... Content Posts support Markdown. Images can be stored via node6-cdn or external URLs. ### Check-in URL: https://zship.ai/docs/integrations/checkin Check-in rules, rewards, daily limits. Backend: node9-checkin-service. Daily check-in is a practical way to lift return visits and habit formation: users get a clear reason to open your app again tomorrow, which compounds into more stable DAU over time. It works best alongside guest access and free credits so the first session feels valuable—see Introduction — Retention and engagement. Check-in is handled by node9-checkin-service. Configurable rules, credit rewards, and daily limits. Admin pages - Check-in Schedule (/checkin-schedule) — Enable/disable, set rules, credit rewards Rules - Daily check-in reward (credits) - Consecutive day bonuses - Schedule (e.g. which days count) User flow Users claim check-in from the dashboard. Credits are awarded via node1-auth integration. ### Email URL: https://zship.ai/docs/integrations/email Configure Resend and Cloudflare Email Service for ZShip, including sender domains, Workers, provider routing, testing, and rollback. ZShip splits transactional email responsibilities between two Workers: - node2-support-service owns email templates, the legacy Resend delivery path, tickets, and feedback. - node11-email-service owns Cloudflare Email Service / Resend providers, per-app routing, test sends, and delivery logs. Keep Resend first in production. Use Cloudflare Email Service for selected projects or as a lower-priority failover provider. 1. Gather the prerequisites Before you start, prepare: - A domain or subdomain using Cloudflare DNS, such as email.example.com - Your Cloudflare Account ID - Access to a Cloudflare account that can deploy Workers, manage D1, and configure the selected domain - A deployed zship-node1-auth Worker - An inbox you control for real delivery tests Check the Wrangler authentication currently available: pnpm exec wrangler whoami If Wrangler is not authenticated, choose the method that fits your environment: - On an interactive workstation, run pnpm exec wrangler login and complete the browser login. - For non-interactive or CI deployment, create a least-privilege API token and store it in the current shell or a CI secret: export CLOUDFLARE_API_TOKEN="your token" export CLOUDFLARE_ACCOUNT_ID="your account ID" Never put an API token in the repository, an Admin provider, an AI prompt, logs, or chat. You need one authentication method, not both. 2. Onboard the sending domain - Open Cloudflare Dashboard → Compute → Email Service → Email Sending. - Select Onboard Domain, then choose the domain or subdomain you will send from. - Confirm the bounce MX, SPF, DKIM, and DMARC records Cloudflare creates. - Wait until Email Sending shows that the domain is ready before deploying. The From address must belong to that exact onboarded domain or subdomain, for example ZShip . Do not reuse the seeded zship.ai sender. Cloudflare references: - Set up Email Sending - Workers API - Send binding restrictions 3. Create D1 and deploy node11 Open the Email Worker directory: cd backend/node11-email-service Create D1 in your Cloudflare account and let Wrangler update the DB binding: pnpm exec wrangler d1 create zship-email --binding DB --update-config pnpm exec wrangler d1 migrations apply zship-email --remote Before deploying, check wrangler.jsonc: - d1_databases[0].database_id is the database you just created, not an ID from the template or another account. - send_email contains a binding named EMAIL. - NODE1_AUTH points to your zship-node1-auth Worker. { "d1_databases": [ { "binding": "DB", "database_name": "zship-email", "database_id": "", "migrations_dir": "migrations" } ], "send_email": [{ "name": "EMAIL" }], "services": [ { "binding": "NODE1_AUTH", "service": "zship-node1-auth" } ] } Deploy the Worker: pnpm exec wrangler deploy Local wrangler dev can simulate the Email binding and report success without delivering a message. Use the deployed Worker for acceptance testing; only add remote: true temporarily when you intentionally need remote development. 4. Connect node2 and Admin The repository template already contains both service bindings: - NODE11_EMAIL in backend/node2-support-service/wrangler.jsonc - EMAIL_SERVICE in apps/admin/wrangler.toml Confirm both point to the zship-node11-email Worker you just deployed, then redeploy node2-support-service and Admin. Without these bindings, Admin cannot manage node11 and node2 cannot fail over to node11. 5. Create the provider in Admin Open Admin → Email Service (/email-service) → Cloudflare fallback: - Create a provider and select Cloudflare Email Service. - Use a stable unique provider key such as cloudflare-email. - Set From to a complete sender on the onboarded domain, such as ZShip . - Keep the provider enabled and save it. A Cloudflare provider does not need an API key in Admin. Delivery authorization comes from the node11 Worker's EMAIL binding. Only Resend providers store a Resend API key in Admin. 6. Configure routing and test delivery Open Routing & test: - Let projects inherit the global route by default; do not replace the existing Resend setup. - For failover, select Failover order and place Resend before Cloudflare. - For an isolated test, assign Cloudflare as the single provider only for a test project; do not change the global production route first. - Send a test message to an inbox you control. - In Delivery logs, confirm a successful status and the expected provider, then verify the From, subject, and body in the inbox. After testing, restore the test project to “Inherit global default” or keep an explicit Resend → Cloudflare failover order. 7. Troubleshooting The UI says sent, but no message arrives - Confirm the test used the deployed Worker instead of a simulated local binding. - Confirm From belongs exactly to the onboarded domain or subdomain. - Check the Email Sending domain status and DNS records. - Check Admin delivery logs and Cloudflare Worker logs. Admin cannot load providers or logs - Check the Admin EMAIL_SERVICE service binding. - Confirm node11 is deployed in the same account under the name zship-node11-email. - Confirm the Admin user has read access to the email module. Does this remove Resend? No. Resend remains the recommended primary provider. Cloudflare Email Service can serve only selected projects or follow Resend as a failover. 8. Rollback Rollback in the lowest-impact order: - Delete the project-level Cloudflare route and restore “Inherit global default”. - Restore Resend-first routing globally or for the affected project. - Disable the Cloudflare provider. Routing rollback does not require deleting the sender domain, D1 database, or Worker. Confirm Resend delivery has recovered before deciding whether to remove Cloudflare resources. 9. Templates, tickets, and feedback - Admin → Templates: password reset, email verification, and custom transactional templates - Admin → Tickets: support tickets - Admin → Email Service: providers, project routes, test sends, and delivery logs ### Image Agent URL: https://zship.ai/docs/integrations/image-agent Conversational image workspace in the official Nuxt app: what it does, what to configure, and how a developer connects it. Image Agent is the conversational image workspace shipped with apps/web-nuxt. A signed-in user describes a new image or a change to an existing one. A dialogue model decides whether to answer, ask a clarifying question, or call an image model. Finished images stay in the conversation and can be edited again. The workspace lives in Node10 (node10-ai-service). Image generation goes through AI Provider Service. Text chat on /dashboard/ai-chat does not create images. AI Playground sends one generation request with the fields the user filled in. Image Agent keeps the conversation, the run, and the image versions, and continues a run after a refresh. What the user can do - Talk through a brief, then generate or edit. - Attach images by upload, drag and drop, a public HTTPS link, or the material library. - Mark a reference and ask for a change against that mark. - Pick an image model, aspect ratio, and the settings that model requires. - Stop a run. A failed image operation follows the Provider refund rules. Dialogue credits already settled stay settled. - Reopen a conversation and see the same messages, references, versions, and progress. Each accepted run may call generate_image, edit_image, and inspect_images. generate_image starts a new image. edit_image changes or combines images already in the conversation. inspect_images loads a closer look before the next edit. One run is capped by the Admin policy: model steps, image operations, credits, and time. How one message runs - The browser calls the Nuxt BFF at /api/image-agent/* with the user's API key (Authorization: Bearer sk-...). The browser never receives the service key. - Node10 stores the conversation and the turn, then starts a Cloudflare Workflow. - The dialogue model runs on the AI Gateway already configured for Node10. - When the model asks for an image, the workflow calls Provider POST /generate with an idempotency key tied to that run and attempt. - Provider bills image credits through Node1, applies content moderation when a moderation config is active, and returns the image URL. - The page reads GET /api/image-agent/turns/:id/events and restores the same run after a reload. Closing the tab does not create a second paid submission for work the workflow already accepted. Where it appears Piece Location User page /dashboard/image-agent in apps/web-nuxt Sidebar entry imageAgent.enabled in apps/web-nuxt/app/app.config.ts Browser API apps/web-nuxt/server/api/image-agent/[...].ts Admin AI → Image Agent Image tasks Providers → Generation Tasks. A task whose client namespace is image-agent links to that run's dialogue charges The Astro, SvelteKit, and TanStack templates do not include this workspace. Copying those apps does not add the page. Set imageAgent.enabled to false in a Nuxt copy to hide the sidebar item. The route remains in the app. The page requires a signed-in user with an API key. Guests are sent through the normal sign-in flow. Enable it Image Agent stays unavailable until the Admin runtime checks are configured and Accept new runs is on. GET /api/image-agent/capabilities returns available: false until then. Existing runs keep the limits they were accepted with. Node10 Deploy node10-ai-service with: Binding or secret Role IMAGE_AGENT_WORKFLOW Cloudflare Workflow that owns the run IMAGE_AGENT_CREDENTIAL_KEY 64-character hex secret. Node10 uses it to encrypt the user API key stored with an in-flight run. Keep it a secret, not a plain var AI, or a gateway API key and base URL Dialogue model upstream PROVIDER1_SERVICE or PROVIDER_URL Image generation and image moderation IMAGE_AGENT_MEDIA Private R2 bucket for user references and annotation images. Do not attach a public domain IMAGE_AGENT_MEDIA_PUBLIC_URL HTTPS base of the signed media route, for example https://media.example.com/image-agent-media. May carry a path prefix; no query or hash IMAGE_AGENT_MEDIA_DEDICATED_HOST Set to true when the host of IMAGE_AGENT_MEDIA_PUBLIC_URL is dedicated to Node10 media. That host then serves only signed media and returns 404 for every other path, so none of Node10's API is exposed there IMAGE_AGENT_MEDIA_SIGNING_KEY 64-character hex secret. Node10 uses it to sign the expiring machine URLs served under IMAGE_AGENT_MEDIA_PUBLIC_URL. Keep it a secret, not a plain var IMAGE_AGENT_LEGACY_MEDIA Optional R2 bucket holding media written before the private bucket existed. Bind it to keep serving old storage='legacy' rows, or omit it to retire them IMAGE_AGENT_MEDIA_QUEUE Queue that downloads, stores, and reviews new media in the background, plus its dead-letter queue (zship-image-agent-media-dlq) IMAGE_AGENT_MEDIA_PUBLIC_URL does not serve the R2 bucket directly. It is the base of an HMAC-signed route on Node10, so that host and path must be routed to node10-ai-service — the official wrangler config puts it on a dedicated custom domain (media.zship.ai, base https://media.zship.ai/image-agent-media). With IMAGE_AGENT_MEDIA_DEDICATED_HOST set to true, that host serves only signed media and returns 404 for every other path, so none of Node10's API is exposed there. A template user points their own host at node10-ai-service and sets both IMAGE_AGENT_MEDIA_PUBLIC_URL and IMAGE_AGENT_MEDIA_DEDICATED_HOST to match. Browsers never use the signed route: they read user media through the same-origin session path GET /api/image-agent/media/:id/content, which is authenticated by the login cookie and never expires. Machines — the dialogue model upstream, the image provider, and the moderation adapter — receive signed URLs minted at the point the request leaves Node10, with at most a seven-day expiry. Signed URLs are never stored on a turn, run, or operation. The Worker cron reconciles runs that are still open and re-enqueues media that stayed pending. Leave that trigger in place. Admin → AI → Image Agent - Choose an enabled dialogue model with valid pricing. A paid model needs a credit ceiling of at least 1. A free model may use a lower ceiling. - Set the ceilings. Defaults are 6 model steps (2–8), 2 image operations (1–3), 100 credits (up to 1000), and 1800 seconds (60–1800). - Set inference if you need to. Output tokens default to 1600, between 256 and 8192. Reasoning effort defaults to the model default. - Confirm the runtime row: workflow, credential encryption, model upstream, image service, and media. - Run the model probe and the media probe. The model probe spends a small dialogue charge. - Turn on Accept new runs and save. Image models In Providers, add an API key for each image provider you want users to see. The picker lists image models from providers that have a key. Video and audio models stay out of this picker. image_delivery is optional. When that provider advertises durable delivery, the run stores the delivered object and uses that URL. When it does not, the run keeps the provider's HTTPS image URL and the model still appears in the picker. Credits Two ledgers apply to one message: Charge Where it is recorded Dialogue tokens Node10, on the Image Agent run as dialogue credits paid. AI Gateway logs use source image_agent Image generation Provider task credits, the same ledger as a direct Playground generation The credit ceiling is the budget for that run. It is not an error message. If the user cannot cover the next dialogue or image call, the run waits or stops under that ceiling. A failed image submission follows Provider's refund path. Retry an Image Agent image task from the conversation, not from the Provider retry button. Reference images Attaching an image returns immediately. POST /media/import and POST /media/upload record a pending row and hand the download, storage, and content review to IMAGE_AGENT_MEDIA_QUEUE. The workspace shows the attachment in a processing state, polls GET /media/:id, and keeps Send disabled until every attachment is ready. A failed attachment shows its reason and can be removed; typing is never blocked. Uploads accept PNG, JPEG, WebP, GIF, and AVIF, up to 10 MiB each. A pasted link is imported when it is public HTTPS: no username, password, or port, and not a local, internal, or raw-IP host. Each redirect is checked the same way, up to three hops. The file must decode as one of those image types. Imported bytes belong to that user and project, and live in the private IMAGE_AGENT_MEDIA bucket. Content moderation Image Agent does not have a separate moderation switch. A Provider POST /generate from a run uses the same content-moderation config as any other generation for that app_key, or the * fallback when the project has no active row. - No active moderation config means the image request is not reviewed. - An active config reviews the configured prompt fields before the image provider is called. A deny decision blocks the image. - If the moderation API times out or returns an error, Fail open lets generation continue. Turn that off to stop the image call when moderation cannot decide. - A flagged result still blocks when Block flagged prompts is on. - The dialogue model call itself is not sent through content moderation. - This path does not use Turnstile. Reference media has its own review. While a queued import or upload is processed, Node10 sends the staged image to Provider's image moderation endpoint with a short-lived signed URL. An enabled config needs an Image moderation API URL on the moderation row; its Image moderation credits are charged to the user when review is performed. Without those two fields the media review is skipped and the attachment becomes ready after it is stored. A denied or blocked attachment never becomes usable as a reference. Setup steps and provider adapters are in AI Provider Service. Use a moderation API key, not the image provider's generation key. Call it from your own UI The Nuxt workspace is the supported client. A custom UI should keep the same boundary: your server holds the service credentials, and the browser only sends the signed-in user's sk- key to your BFF. Official BFF routes, relative to /api/image-agent: Method Path Purpose GET /capabilities Whether new runs are accepted, the dialogue model, and the ceilings GET /conversations This user's conversations POST /conversations Create one. Body: id, title GET /conversations/:id Conversation plus turns POST /conversations/:id/update Rename, or { "archived": true } after the active run has stopped POST /media/import Body: { "url": "https://..." }. Returns { id, state, url } right away POST /media/upload Raw image bytes with an image Content-Type. Same response shape GET /media/:id Submission state: pending, ready, failed, or blocked GET /media/:id/content Bytes for an image owned by this user. sk- clients use the API key; browsers may use the login cookie instead POST /turns Save the user message. See the body below POST /turns/:id/generate Start the workflow. Optional body: { "limits": { "maxCredits": 20, "maxOperations": 1 } } GET /turns/:id/events?after=0 Incremental events. after is the last event id you have stored GET /turns/:id/status Current turn, including workflow progress POST /turns/:id/cancel Stop new work. Accepted image work still settles POST /turns/:id/resume Continue a run that is waiting on the user or on credits Create the turn before generate: { "id": "turn-uuid", "conversation_id": "conversation-uuid", "prompt": "Replace the background with a night street", "references": [], "reference_ids": [], "annotations": [], "mode": "agent", "execution": "workflow", "request": { "provider": "your-provider-key", "model": "your-image-model" }, "agent_options": { "promptField": "prompt", "referenceField": "image_urls", "referenceArray": true, "maxReferences": 4 } } request is the image model's public parameter object. Do not send credentials, callback URLs, or idempotency fields in it. agent_options must match that model's prompt field and reference field. The official workspace fills both from the Provider catalog; a custom client has to do the same mapping. useImageAgent in apps/web-nuxt is the reference for conversation restore, event polling, and credit refresh. Prefer that composable over a second client. Local preview In nuxt dev only, open /dashboard/image-agent?mock=1&case=edits. Other case names cover an empty thread, a pending run, insufficient credits, cancellation, and a completed delivery. A production build ignores mock. When a run will not start What you see What to check Sidebar item missing imageAgent.enabled in the Nuxt app config Capabilities say unavailable Admin: dialogue model, pricing, Accept new runs, and every runtime dependency No image models in the picker Provider API keys, and that the model is an image model Import link fails Public HTTPS, a real image, at most 10 MiB, no local or raw-IP host Attachment stays in processing IMAGE_AGENT_MEDIA_QUEUE producer/consumer, the DLQ, and IMAGE_AGENT_MEDIA_SIGNING_KEY Image never starts, no Provider task Workflow binding, credential secret, and the user's credit balance Provider task exists, dialogue charge is elsewhere Open the task's dialogue-charge link, or AI → Image Agent and the run id ### Notify URL: https://zship.ai/docs/integrations/notify Bark, Feishu, DingTalk. Backend: node4-notify-service. Notifications are handled by node4-notify-service. Supports Bark, Feishu, DingTalk, and webhooks. Admin pages - Notify Settings (/notify-settings) — Configure channels Channels - Bark — iOS push notifications - Feishu — Feishu/Lark webhook - DingTalk — DingTalk webhook Usage Backend services (e.g. pay, support) can trigger notifications via node4. Configure webhook URLs and keys in admin. ### Crisp URL: https://zship.ai/docs/integrations/crisp One Crisp chatbox integration shared by multiple ZShip app_key tenants. Crisp support chat is managed as a platform-level integration. You configure one Crisp Website ID in Admin, then every eligible ZShip frontend loads the chatbox automatically and writes its app_key into Crisp session data. Why no new Worker Crisp already stores the conversation history and inbox state. ZShip only needs to store the integration config: - enabled or disabled - Crisp Website ID - all-projects or selected-projects targeting - optional Crisp inbox shortcut That config lives in node1-auth-service under t_admin_setting.crisp_integration_config. A separate Worker would add another deploy target without owning any unique durable data. Admin setup - Open Admin → Crisp Chat (/crisp-settings). - Enable Crisp and paste the Website ID from your Crisp dashboard. - Keep All projects selected if every current and future app_key should use the same inbox. - Save. New apps based on the ZShip template do not need a separate Crisp script. Use Selected projects only when you want a gradual rollout or need to keep specific tenants off chat. Frontend behavior The ZShip templates call node1-auth-service for the current app's public Crisp config. If enabled, the template injects Crisp and sends: - app_key - project title and domain - user email or guest ID when available - a zship-app:{app_key} Crisp segment This is what lets one Crisp inbox handle multiple ZShip projects while keeping conversations filterable by tenant. Do we store chat records in D1? No, not by default. D1 stores only configuration. Crisp remains the source of truth for messages, assignment, inbox state, and transcripts. Add a Crisp Webhook and a new storage path only if you need local compliance retention, analytics, or support automation that cannot be handled inside Crisp. Crisp dashboard notes If the same Crisp Website ID is used on multiple domains, review Crisp's chatbox security settings. In particular, domain-locking must match the domains where your ZShip apps run. ### Payment URL: https://zship.ai/docs/integrations/payment Pay channels, price plans, Stripe catalog fetch, project bindings, and localized pricing copy. Payments are handled by node3-pay-service with Stripe, Creem, Dodo Payments, Waffo Pancake, and configurable plans. The usual model is: create pay channels first, map each plan to the provider's product IDs in Price Config, then bind every project (app_key) to a channel so checkout and webhooks use the right credentials. Each app is bound to exactly one provider channel. Recommended setup order - Pay Channels (/pay-channels) Add Stripe or Creem channels (keys, webhook secrets, etc.). One Stripe account can back one or many channel rows in Admin if you split by environment or brand. - Price Config (/price-config) Create or edit plans: amount, billing interval, credits, feature flags. For Stripe, align each plan with Product ID and Price ID (below). - Project ↔ channel binding (same Pay Channels page) Bind each app_key to a Stripe or Creem channel. New projects stay non-functional for payments until this binding exists—the system must know which channel credentials to use. This lets you ship new sites quickly and reuse one Stripe catalog across many projects instead of rebuilding a full store for every launch; add or split channels when you are ready. Stripe Product ID and Price ID In the Stripe Dashboard (Products), each price has a Price ID (price_...) under a Product ID (prod_...). Admin Price Config must store the correct IDs for checkout and webhooks to line up. To avoid manual copy/paste mistakes, use Fetch products / prices from Stripe in the price editor: - Calls the Stripe API using the selected pay channel’s secret key and lists products/prices you can pick and persist. - Prerequisites: choose the app (app_key) in the editor and ensure that app already has a Stripe channel binding in /pay-channels; otherwise fetch is blocked. - If you run multiple Stripe channels, you can choose which channel’s credentials to use for the catalog fetch (vs. the app’s default binding). Fetch only wires IDs to Stripe’s catalog; customer-facing copy is still maintained in Admin (next section). Localized pricing copy Plans support per-locale display fields (e.g. title, description, badge, feature bullets), typically stored as structured display_i18n keyed by locale (en, zh, …). - You author and maintain each locale’s text in the Admin UI (including any bulk / side-by-side editors). Stripe does not supply localized marketing strings for these cards. - Where AI Gateway is enabled, optional helpers may fill missing locales; always review before publishing. The user-facing app picks the locale from site or user settings and falls back when a locale is missing (behavior follows the storefront implementation). Creem Enable Creem under Pay Channels and add the API key; project binding to that channel is still required for the app to process Creem payments. Plan fields follow Creem’s model (not Stripe Product/Price IDs—use the Admin form and API as reference). Dodo Payments - In the Dodo dashboard, go to Developer → API Keys and create a key. Test keys only work against the test API; live keys only against live. - In Admin Pay Channels, create a Dodo Payments channel with the API key and set Environment (test or live) — required, it selects the API host. - Add a webhook endpoint https:///dodo/webhook in Dodo and copy its signing secret (whsec_…) into the channel's webhook secret field. Subscribe to: payment.succeeded, subscription.active, subscription.renewed, subscription.cancelled, subscription.expired, subscription.on_hold, subscription.updated, subscription.plan_changed, refund.succeeded, refund.failed, dispute.opened, dispute.lost. - In Price Config, set each plan's Dodo Product ID (pdt_…). - Bind the channel to the app (app_key). Buyers manage Dodo subscriptions through the Dodo customer portal (/dodo/portal returns a session link). In-app plan change/cancel is Stripe-only. Waffo Pancake - In the Waffo dashboard, copy your Merchant ID (MER_…) from API & Development — not storeId. - Create an API key and download the private key (PEM). Create a Waffo Pancake channel in Admin Pay Channels with the private key and merchant ID; environment is informational. - Create products in Waffo and publish them to production before live checkout. Put each product ID (PROD_…) into Price Config. - Register the HTTP webhook https:///waffo/webhook once per environment (testMode true/false). Subscribe to: order.completed, subscription.activated, subscription.renewed, subscription.recovered, subscription.canceling, subscription.uncanceled, subscription.canceled, subscription.past_due, refund.succeeded, refund.failed. Webhooks are verified with the embedded Waffo platform public keys; you can optionally paste a webhook public key on the channel to override them. - Bind the channel to the app. Notes: test card 4576750000000110; successUrl must be absolute https; there is no auto-redirect after payment (the buyer clicks Done); the customer portal is the shared Waffo consumer portal with magic-link login (/waffo/portal returns it). In-app plan change/cancel is Stripe-only. Provider limitations - ZShip coupons are not applied for Dodo or Waffo checkout (couponsEnabled is false when the app's bound provider is dodo/waffo). - The payment simulator only covers Stripe and Creem. - One provider per app — binding a second provider replaces the first. After you create a project Whenever you add a new project (app_key), open Pay Channels and confirm: - A binding points that app to the correct Stripe, Creem, Dodo, or Waffo channel. - Keys and webhook endpoints match production. Without a binding, checkout or webhooks may not work for that site. Admin routes - Pay Channels (/pay-channels) — channels, project bindings, webhook guidance - Price Config (/price-config) — plans, Stripe fetch, localized copy, optional per-channel Stripe overrides ### Subscription Management URL: https://zship.ai/docs/integrations/subscription-management Stripe subscriptions, plan changes, recurring credits, webhook health, and launch checks. This guide is for site owners and delivery teams. It explains what the current ZShip subscription system supports, what must be configured, how to validate it before launch, and where to look when something behaves unexpectedly. Subscription management spans multiple shared services and layers: - backend/node3-pay-service: checkout, Stripe subscription mutations, webhook processing, and subscription change history. - backend/node1-auth-service: user subscription read model, subscription credits, and API key entitlement resolution. - packages/nuxt-common-layer: user dashboard subscription page, Change Plan modal, cancel and resume actions. - packages/nuxt-admin-layer: admin plans, pay channels, webhook audit, webhook event health, and user subscription management. - backend/zship-provider1-service: optional subscription-based provider/model access control. Current capabilities Stripe subscriptions Stripe is the provider with complete subscription management support. The system supports: - Initial subscription checkout. - Renewal invoices refreshing the billing period and subscription credits. - Immediate upgrades with Stripe proration invoices. - Pending payment or SCA flows for immediate upgrades. - Automatic cleanup when a pending update expires or fails. - Downgrades and interval changes scheduled for the end of the current period. - Stripe subscription schedules for end-of-period plan changes. - Cancel at period end. - Resume by clearing cancel_at_period_end. - Synchronization of statuses such as active, past_due, unpaid, paused, canceled, incomplete, and incomplete_expired into the Node1 read model. Creem and mock subscriptions Creem and mock flows can be used for payments, subscription state, or credit tests, but they do not support Stripe subscription management operations. The UI follows that boundary: - Change Plan / Cancel / Resume are shown only when the provider is stripe and provider_subscription_id exists. - Creem and mock subscriptions do not show Stripe-only actions. - Legacy rows with an empty provider but a Stripe subscription id are treated as legacy Stripe subscriptions. Data ownership Node3 Pay is the payment operation source node3-pay-service talks to Stripe and owns payment mutations: - Create Stripe Checkout Sessions. - Update Stripe subscriptions. - Create or update Stripe subscription schedules. - Verify Stripe webhook signatures. - Write t_webhook_audit. - Write t_subscription_change. - Sync the latest subscription state to Node1. Important tables: - t_price_config: plans, Stripe price ids, and subscription credit settings. - t_price_config_stripe_channel: Stripe price overrides for multiple channels. - t_subscription_change: upgrade, downgrade, cancel, resume, pending payment, and related history. - t_webhook_audit: raw webhook event audit. Node1 Auth is the read model and entitlement source node1-auth-service stores the state read by users, AI providers, API keys, and the credit system. Important tables: - t_user_subscription: current subscription, pending plan, cancel-at-period-end state, credit cadence, and related fields. - t_credit_account: permanent credits and subscription credits. - t_subscription_credit_cycle: monthly subscription credit cycle records. Important behavior: - /credits/balance returns the current balance and subscription detail. - /internal/credits/subscription-sync receives lifecycle syncs from Node3 and does not grant credits. - /credits/subscription-renew refreshes the period and credits after a real renewal invoice. - /keys/resolve exposes subscriber entitlements to Provider Service only for valid subscriptions. Plan configuration Create subscription plans in Admin Price Config. Common required fields: Field Purpose app_key Tenant project key price_type Stable plan id, for example pro_monthly or pro_yearly is_subscription Must be enabled for subscription plans stripe_price_id Stripe Price ID, unless a channel override is used amount / currency / interval Used for display and plan-change classification tier Entitlement tier, for example basic, pro, or team credits Compatibility field and fallback value credit_cadence billing_period or monthly credit_amount Credits granted each subscription credit cycle credit_policy reset or accumulate credit_anchor Usually keep the default subscription_start Subscription credit rules credit_cadence = billing_period: - Grants subscription credits once per real billing renewal. - Fits monthly plans that grant monthly credits, or annual plans that grant the full annual amount at renewal. credit_cadence = monthly: - Stripe still controls the real billing period. - Node1 grants subscription credits monthly using next_credit_reset_at. - Fits annual plans that grant credits monthly, such as mock_annual_monthly_credits. credit_policy = reset: - Each grant resets subscription credits to credit_amount. credit_policy = accumulate: - Each grant adds credit_amount to the current subscription credit balance. Lifecycle Initial purchase - The user selects a subscription plan on Pricing. - The frontend calls local /api/pay/checkout. - Node3 creates a Stripe Checkout Session and stores email, app_key, price_type, tier, and credits in metadata. - Stripe sends checkout.session.completed. - Node3 writes the payment record and calls Node1 to activate the subscription. - Node1 writes t_user_subscription and grants the initial subscription credits. Renewal - Stripe sends invoice.paid. - Node3 processes only real renewal invoices: - Skip subscription_create; checkout handles the initial grant. - Skip subscription_update; upgrade proration invoices must not grant renewal credits. - Process subscription_cycle and keep compatibility with legacy subscription. - Before renewal, Node3 resolves the latest plan from the invoice or the current Stripe subscription price. - If a scheduled plan change has taken effect in Stripe before customer.subscription.updated arrives, invoice.paid can still sync Node1 first. - Node1 refreshes subscription credits using credit_cadence and credit_policy. Immediate upgrade Plan changes to a higher amount are treated as immediate upgrades: - Node3 calls Stripe subscriptions.update. - It uses proration_behavior: always_invoice. - It uses payment_behavior: pending_if_incomplete. - If Stripe succeeds immediately, Node3 syncs the new plan to Node1. - If payment or SCA is required, Stripe sets subscription.pending_update, and Node3 records the pending plan. - The UI shows a Complete Payment action. - Successful payment is handled through customer.subscription.pending_update_applied. - Expired or failed pending updates are handled through customer.subscription.pending_update_expired. When a pending update or pending plan already exists, further plan-change requests return a controlled pending-change response instead of falling into a Stripe pending-update conflict. End-of-period downgrade or interval change Lower amount changes, or monthly/yearly interval changes, are scheduled for the period end: - Node3 creates or updates a Stripe subscription schedule from the current subscription. - The current phase keeps the current price until current_period_end. - The next phase uses the target price. - The schedule uses end_behavior: release, and the target phase uses iterations: 1, so Stripe releases the schedule after the target phase while the subscription continues. - Node1 stores pending_plan_id, pending_effective_at, and subscription_schedule_id. - At period end, customer.subscription.updated or renewal invoice.paid confirms the target plan and clears pending state. Cancel and resume Cancellation is not immediate deletion: - Cancel sets cancel_at_period_end = true in Stripe. - Node1 keeps the current plan and shows that it will cancel at period end. - Resume clears cancel_at_period_end. - When the subscription really ends, Stripe sends customer.subscription.deleted; Node1 marks it canceled and clears subscription credits. Stripe webhook setup The webhook URL is usually: https:///stripe/webhook Admin Pay Channels shows a copyable webhook URL and suggested events for the current project. Required Stripe events Event Purpose checkout.session.completed One-time payments and initial subscription activation checkout.session.async_payment_succeeded Delayed payment success handling invoice.paid Subscription renewals and billing-period credit refresh customer.subscription.created New subscription state sync and debugging customer.subscription.updated Plan changes, cancel marker, and status sync customer.subscription.pending_update_applied Pending upgrade payment succeeded customer.subscription.pending_update_expired Pending upgrade expired cleanup customer.subscription.deleted Subscription ended charge.refunded Credit and commission clawback Recommended Stripe events Event Purpose invoice.payment_failed Failed renewal alerts invoice.payment_action_required SCA or manual payment action required invoice.finalization_failed Invoice finalization failures customer.subscription.paused Paused subscription status sync customer.subscription.resumed Resumed subscription status sync customer.subscription.trial_will_end Trial ending reminders checkout.session.async_payment_failed Delayed payment failure alerts payment_intent.payment_failed Payment failure audit billing_portal.session.created Billing Portal launch audit Webhook health Admin /payments includes: - Webhook audit log modal. - Webhook key event health modal. - Copyable event names. Health shows whether each key event has ever been received. On a new production site, some events may be missing until they naturally occur, but Stripe Dashboard must still subscribe to the required events. Admin operations Price Config Site owners should configure subscription plans first: - Open Admin -> Payments -> Price Config. - Create or edit a subscription plan. - Fill tier, credits, credit_cadence, credit_amount, and credit_policy. - Fill the Stripe Price ID, or configure a channel override. - Save; Node3 syncs plan settings to Node1 project settings for subscription credit logic. User subscription management Admin -> Users -> Manage Subscription shows: - Current plan, tier, and status. - Next billing time and time remaining. - Cancel-at-period-end notice. - Pending plan change notice. - Subscription change history. Only Stripe subscriptions can be changed, canceled, or resumed here. Creem, Dodo Payments, Waffo Pancake, and mock subscriptions show a boundary notice and do not expose Stripe-only actions; Dodo and Waffo subscriptions are managed in their respective customer portals. User Change Plan On the user dashboard Subscription page: - Change Plan is a button. - Clicking it opens a modal for target plan selection. - Plans are not listed directly on the page. - Plan card action buttons are bottom-aligned. - If an upgrade requires payment, the UI shows Complete Payment. AI Provider subscription model access When zship-provider1-service is enabled, Provider or model access can be restricted by subscription plan: - Admin -> Providers -> Subscription Plan Model Access. - Configure allowlists by app_key and plan_id. - Both public model IDs and provider-facing model IDs after model_selector are checked. - Model list requests pass subscription_plan_id only for active subscriptions. - Generation uses the active subscription resolved from the API key as the final authority. This prevents users from seeing a model that generation later rejects, and prevents model-selector rewrites from bypassing subscription model restrictions. Mock validation Mock flows are useful for local or internal validation of subscription credit logic. They do not represent Stripe-manageable subscriptions. Common scenario: - mock_annual_monthly_credits: annual subscription with monthly subscription credit grants. - Use it to validate credit_cadence = monthly, next_credit_reset_at, and monthly cycle jobs. Boundaries: - Mock provider does not support Stripe plan changes. - Mock provider does not support Stripe cancellation. - The UI hides Stripe-only actions for mock rows. Launch checklist Migrations Before deployment, apply these migrations: - backend/node1-auth-service/migrations/0023_subscription_credit_cadence.sql - backend/node1-auth-service/migrations/0024_subscription_change_state.sql - backend/node3-pay-service/migrations/0012_subscription_credit_settings.sql - backend/node3-pay-service/migrations/0013_subscription_management.sql - If Provider subscription model access is enabled: backend/zship-provider1-service/migrations/0011_subscription_model_access.sql Environment and bindings Confirm: - node3-pay-service can reach Node1 through NODE1_AUTH service binding or NODE1_AUTH_URL. - ZSHIP_KEY matches across internal services. - Stripe Pay Channel has the correct secret key and webhook secret. - Frontend cloud API URLs point to the correct pay/auth/provider environment. - Admin and user site use the same app_key. Stripe test mode validation Before real charges, run at least one Stripe test mode pass: - Register or log in as a user. - Create an initial subscription. - Confirm checkout.session.completed arrives and Node1 has an active subscription. - Trigger renewal invoice.paid and confirm credits follow the current plan. - Upgrade immediately and confirm the proration invoice does not grant renewal credits. - Simulate payment action required and confirm Complete Payment plus pending state. - Let or simulate pending update expiration and confirm pending state clears. - Downgrade or switch interval and confirm pending plan is shown for period end. - At period end, confirm the new plan and target credit settings are active. - Cancel and confirm cancel_at_period_end. - Resume and confirm the cancel marker disappears. - End or delete the subscription and confirm Node1 marks it canceled and clears subscription credits. Common issues Only Stripe subscription changes are supported The subscription is not a Stripe provider subscription, or it has no Stripe subscription id. What to check: - For mock or Creem, this is expected; Stripe plan changes cannot be performed there. - For legacy Stripe rows, check t_user_subscription.provider_subscription_id. - Rows with an empty provider but a Stripe subscription id are treated as legacy Stripe subscriptions. Credits increased after an immediate upgrade Check the billing_reason on invoice.paid. Current renewal logic processes only: - subscription_cycle - legacy-compatible subscription It skips: - subscription_create - subscription_update If credits still look wrong, check whether an older deployment is still processing webhooks. Downgrade took effect but credits still use the old plan The current logic syncs Stripe's current price before renewal. Check: - Whether the Stripe subscription item price is already the target price. - Whether t_price_config or channel override can map that Stripe price id to the target plan. - Whether customer.subscription.updated or invoice.paid reached Node3. - Whether Node1 t_user_subscription.credit_amount changed to the target plan. Delivery guidance The code paths have static verification, but Stripe is an external state machine. Run Stripe test mode before production billing. Recommended site-owner handoff wording: - There are no known blocking issues at the code level. - Stripe subscription management supports the main lifecycle. - Site owners must configure webhooks and complete test mode validation before real charges. - Creem and mock do not support Stripe subscription management actions; that is a product boundary, not an error. Include these handoff details: - Stripe webhook URL. - Required event list. - Current app_key. - price_type to Stripe price id mapping. - Test account and test mode validation notes. ### Storage URL: https://zship.ai/docs/integrations/storage File upload, CDN. Backend: node6-cdn-service. File storage and CDN are handled by node6-cdn-service. Uses Cloudflare R2 for object storage. Configure in Admin. R2 bucket and CDN settings are managed in the admin panel. Usage - Dashboard islands can upload images (e.g. profile avatar) - Blog images, CDN assets - API keys required for upload (user or admin) ### Branding URL: https://zship.ai/docs/customization/branding Site name, logo, colors, SEO. Config files and overrides. Customize your ZShip site's brand without code changes where possible. Config file (no rebuild for backend vars) Feature Config File Fields Astro brand identity apps/web-astro/zship.app.json appKey, domain, siteUrl, siteName, tagline, description, brand.*, links.*, dashboard.url Nuxt brand identity apps/web-nuxt/zship.app.json appKey, domain, siteUrl, siteName, brand.*, links.*, contactEmail Nuxt header / footer apps/web-nuxt/zship.app.json header.*, footer.*, dashboard.features.* Dashboard brand packages/dashboard/config/brand.ts BRAND_NAME, STORAGE_PREFIX Analytics apps/web-astro/zship.app.json / apps/web-nuxt/zship.app.json analytics.googleAnalytics.measurementId, analytics.plausible.*, Nuxt also supports analytics.clarity.projectId Analytics (Google Analytics 4 & Plausible) Public pages that use the shared Base layout load scripts from apps/web-astro/src/components/AnalyticsHead.astro. Nothing is injected unless you set valid values in apps/web-astro/zship.app.json under analytics. Google Analytics 4 - Set analytics.googleAnalytics.measurementId to your GA4 Measurement ID (format G- followed by alphanumeric characters). - Leave it empty or omit googleAnalytics to disable gtag.js. Plausible - Set analytics.plausible.domain to the same domain you registered in Plausible (e.g. yoursite.com). - Default script URL is https://plausible.io/js/script.js. - Self-hosted Plausible: set scriptSrc to your instance’s script URL (e.g. https://plausible.example.com/js/script.js). - Reverse proxy: set apiEndpoint to your proxied event endpoint; it is emitted as the data-api attribute on the script tag. You can enable GA4, Plausible, both, or neither. Rebuild the web app after changing zship.app.json. How to rebrand - Edit apps/web-astro/zship.app.json — change appKey, domain, siteName, brand.*, links.*, and analytics.* - Edit packages/dashboard/config/brand.ts — change BRAND_NAME (dashboard sidebar) - Replace /public/logo.svg and /public/favicon.ico with your assets - For Nuxt apps, edit apps/web-nuxt/zship.app.json; keep apps/web-nuxt/app/config/site.ts as the manifest adapter Component overrides Place files in apps/web-astro/src/overrides/ to override shared components without modifying packages. The zship-resolver plugin auto-resolves overrides. See apps/web-astro/src/overrides/README.md for details. ### i18n URL: https://zship.ai/docs/customization/i18n Multi-language support. Translation files and locale config. ZShip supports multiple languages via i18n configuration. Translation files - Web: apps/web-astro/src/i18n/locales/ — en.json, zh-CN.json, etc. - Dashboard: packages/dashboard/ or shared i18n - Admin: apps/admin/i18n/locales/ Adding a language - Add a new JSON file (e.g. ja.json) to the locales folder - Add the locale to utils.ts or the i18n config - Update routing if using locale-prefixed paths (e.g. /ja/) Keys Translation keys follow a nested structure. Add new keys for new UI strings. Existing keys can be overridden per locale. ### Admin RBAC & operator accounts URL: https://zship.ai/docs/development/admin-rbac How ZShip admin permissions work (legacy secret vs operators), passwordless login (magic link & demo links), platform settings, permission keys, and scoped admin accounts. ZShip’s management console supports two ways to sign in (email/password or legacy secret). Passwordless flows (email magic link and customer demo links) are additional paths that still issue a normal admin JWT—see Passwordless access and platform settings. Mode Login JWT Typical use Legacy “super admin” POST /admin/login body { "secret": "" } No admin_kind: "operator", no permissions array Bootstrap, break-glass, full access Operator (RBAC) POST /admin/login body { "email": "...", "password": "..." } admin_kind: "operator" + permissions: string[] Day-to-day staff with least privilege Everything below focuses on operators and how permissions are enforced. Legacy admins are intentionally all-powerful: treat ADMIN_SECRET like a root password. Use case: let people observe operations without mutating data When someone needs to see how the system is running (dashboards, analytics, logs, user lists, etc.) but must not freely edit or delete production data or critical settings, create an operator (RBAC) account instead of sharing ADMIN_SECRET. In Admin operators (usually /operators), create the user and grant admin..read (read-only) for the modules they need—commonly dashboard, logs, users, and so on—then add .write or bare admin. only where changes are truly required. For read-only access, many APIs redact sensitive fields (see Read-only operators and sensitive data below). When access should end, an admin with admin.operators.write can deactivate or delete the operator or tighten permissions; the user typically needs to sign in again for JWT changes to apply. There is no automatic time-based expiry—you control scope explicitly through accounts and permission strings. Permission strings Operators carry a JSON array of permission keys on the JWT (and in D1 as t_admin_operator.permissions). Common patterns: - admin. — full access to that module (read + write style routes), backward compatible. - admin..read — list/detail style admin APIs only. - admin..write — create/update/delete style admin APIs (often also allows read routes in practice via shared guards; write is the stricter gate for mutations). - "*" — super-operator: every module (still an operator account, not legacy secret). Module ids must align with the backend catalog, for example: dashboard, users, credits, projects, keys, logs, verifications, guests, oauth, referrals, operators, plus cross-service surfaces such as pay, support, blog, notify, cdn, site, prompt, checkin, provider, core. Authoritative constants live in the template repo at backend/node1-auth-service/src/constants/admin-permissions.ts (ADMIN_ROUTE_ACCESS maps each POST /admin/... path to { module, need: 'read' | 'write' }). Where enforcement happens - node1-auth-service — verifies JWT on /admin/* (except login/me). Operator JWTs must satisfy the route’s module + read/write requirement. - Other Workers (pay, support, checkin, provider, …) — use service bindings + requestAuthService / adminJwtAnyOfForModule and their own route tables; same permission strings. - Admin UI (nuxt-admin-layer) — canReadModule / canWriteModule hide or disable controls; server checks and response redaction still win if someone calls APIs directly. Read-only operators and sensitive data If an operator has only .read on a module (no .write and no bare admin.), many list/detail APIs redact secrets and PII (masked emails, placeholder secrets, etc.). Implementation is centralized in packages/backend-utils/src/admin-sensitive-redact.ts; the repo doc docs/rbac/redaction.md lists endpoints and fields. Adding operator accounts (recommended path) Prerequisites - D1 migration applied so t_admin_operator exists (backend/node1-auth-service/migrations/0011_admin_operators.sql in the template). Using the admin UI - Sign in as a legacy super admin (ADMIN_SECRET) or an operator that already has admin.operators (or *). - Open Admin operators (route /operators in apps that extend nuxt-admin-layer). - Create a user: email, password (minimum length enforced by API), and select permissions (the UI mirrors the catalog). First operator when you only have ADMIN_SECRET - Log in with the secret once. - Use Admin operators to create real accounts; then prefer operator login for routine work and keep ADMIN_SECRET in a vault. Bootstrap via SQL (advanced) If you must insert the first row by hand: store bcrypt password_hash compatible with node1-auth-service (hashPassword in src/utils/password.ts), and set permissions to a valid JSON array string, e.g. ["admin.dashboard","admin.users.read"]. Wrong hashes or malformed JSON will break login. Prefer the UI when possible. Managing the operators module - admin.operators.read — list operators. - admin.operators.write — create, update, deactivate/delete operators. Without admin.operators, operators cannot open the operators page or call those APIs. Passwordless access and platform settings Where in the UI In apps that extend nuxt-admin-layer, the sidebar Admin group includes: - Admin operators — route /operators; create and manage operator accounts (requires admin.operators). - Platform settings — route /login-settings; configure the public admin URL, email magic link, and customer demo links (same operators module permission as operator management). What “platform settings” covers - Public admin URL — Ensures magic-link emails and demo-link previews use the correct https://… origin. Values may come from the settings store (D1) and/or the auth Worker env ADMIN_PUBLIC_ORIGIN (see Domains & environment). - Email magic link — When enabled, a user enters a registered operator email on the login page and requests a one-time sign-in email. The backend resolves t_admin_operator by email; verification issues an operator JWT (admin_kind: "operator"), equivalent to password login for RBAC. - Customer demo links — Long-lived passwordless URLs (?demo_token=… on the admin login page). Two modes at creation time: - Classic demo — Internal demo identity (admin_kind: "demo") with permissions stored on the demo-link row (default is dashboard read-only). - Bound to an operator (optional) — Bind an active operator; exchanging the token issues an operator JWT with that account’s permissions at exchange time, and includes demo_link_id so revoking the demo link invalidates the session on the next authenticated API call. JWTs and permission changes - Admin JWTs (password, magic link, demo link) embed permissions at issue time. Editing an operator’s permissions does not refresh an existing session until the JWT expires or the user obtains a new token (new login, new magic link, or new demo-token exchange). - Revoking a customer demo link blocks sessions that were issued from that link immediately at the API layer (while the JWT might still be unexpired). Security notes - Demo links bound to powerful operators are sensitive: they approximate sharing that operator’s access for the JWT lifetime (often long for demo TTL). Use narrow operator accounts for external demos when possible. - Classic demo URLs are not one-time by default; the same demo_token can be exchanged again until the link is revoked or optional expiry is reached. Related D1 migrations (node1-auth-service) - Operators table: 0011_admin_operators.sql - Demo links table: 0014_admin_demo_link.sql - Optional operator binding on demo links: 0015_admin_demo_link_operator.sql Practical tips - Prefer narrow permissions (*.read + a few *.write) over * for contractors or support. - Remember legacy login bypasses RBAC entirely; protect ADMIN_SECRET and rotate if leaked. - After permission changes, the operator may need to log in again to get a fresh JWT. See also - 06. Initialize your system — first login with ADMIN_SECRET - Template reference: backend/node1-auth-service/README.md (section on operators & RBAC) - Template reference: docs/rbac/redaction.md (read-only response redaction matrix) ### Building a custom admin for special business logic URL: https://zship.ai/docs/development/custom-admin-platform Extend the official admin in apps/admin with Nuxt layers—do not fork packages/nuxt-admin-layer—so upstream UI updates never conflict with your code. The official management UI and shared behavior live in packages/nuxt-admin-layer. That package is maintained by ZShip and receives ongoing updates (routes, components, RBAC wiring, etc.). Do not modify nuxt-admin-layer Treat packages/nuxt-admin-layer as read-only in your fork: - Changes there are hard to merge when we ship template updates. - You would be editing “vendor” UI that is meant to stay aligned with the official admin. If you need business-specific screens, APIs, or workflows, implement them outside that package. Put custom logic in apps/admin The default admin app is apps/admin. It already extends the layer in nuxt.config.ts: export default defineNuxtConfig({ extends: ['../../packages/nuxt-admin-layer'], // ... }) Add your special behavior here (or in another Nuxt app that uses the same extends pattern): - New pages under apps/admin/app/pages/ (or the equivalent directory your Nuxt version uses) - App-local components, composables, and middleware - Branding, runtimeConfig, and environment-specific settings This keeps your code in your app, while nuxt-admin-layer stays the upstream shell. How Nuxt layers override each other With extends, the child app wins when both define the same resource (for example a page or component with the same path/name). Concretely: - If you provide a matching override in apps/admin, that version is used. - If you do not override, the implementation from nuxt-admin-layer is used. So you can selectively replace official pages or components without copying the entire admin package—and official updates to unrelated parts of the layer still apply. Practical tips - Prefer adding new routes and modules under your app rather than patching files under packages/nuxt-admin-layer. - Keep backend rules (permissions, Service Bindings) consistent with Admin RBAC; the UI is not the authority for security. - To scaffold a separate admin app, use the create-admin skill in .cursor/skills/ (copy from the official admin template and point extends at the layer). Official admin UI (reference screenshots) The following images show the official management surfaces shipped with nuxt-admin-layer. Your apps/admin extends this layer; you add or override pages in the app, not in the package. Workbench & projects AI & gateway Operations Operators (RBAC) Users & access Support & billing Content & storage See also - Admin RBAC & operators - Development skills — includes create-admin ### app.zship.ai (post-purchase site) URL: https://zship.ai/docs/development/platform-zship-app What you can do on app.zship.ai: orders, licensing, delivery, downloads, and support. After you sign in at app.zship.ai, you can use: Area What it covers Orders & billing Order history, payments, and billing-related information Licensing Platform license and entitlement status GitHub delivery Request or manage GitHub repo access bundled with a purchase (when applicable) Downloads & assets Download center and resources covered by your plan Support Open and track support tickets ### Development Skills URL: https://zship.ai/docs/development/skills ZShip development skills guide: AI-powered tools for common development tasks. ZShip provides a set of development skills that help you perform common development tasks more efficiently. These skills are AI-powered workflows that guide you through complex operations while ensuring compliance with ZShip conventions. What are Skills? Skills are specialized AI workflows stored in .cursor/skills/ that provide step-by-step guidance for: - Creating new apps and services - Adding features to your dashboard - Customizing branding - Reviewing code for compliance - Syncing with upstream updates - And more... When you ask the AI assistant to perform a task, it automatically uses the relevant skill to ensure best practices and convention compliance. Available Skills Core Development Skills onboard First-time setup guide for ZShip secondary development - Explains architecture and read-only boundaries - Security checklist (secrets, API keys) - Naming rules and conventions - Multi-tenant isolation requirements Use when: Starting a new ZShip project or onboarding new developers. create-app Create a new brand site by copying from the official web template - Copies apps/web-astro/ to your new app directory - Sets up app_key, branding, and configuration - Configures unique ports and package names Use when: Starting a new frontend project for a new brand. create-admin Create a custom admin console by copying from the official admin template - Copies apps/admin/ to your custom admin directory - Sets up admin-specific configuration - Configures admin routes and permissions Use when: You need admin functionality with custom modifications. create-service Create a new backend service (Cloudflare Worker + Hono + D1) - Generates complete service structure - Sets up Hono framework, D1 database, migrations - Enforces naming conventions (never use nodeXX-* format) - Includes multi-tenant isolation patterns Use when: You need custom server-side logic beyond what official nodeX services provide. Feature Development Skills add-page Add a new page to your ZShip app (Astro or Nuxt) - Creates page component with proper structure - Sets up API proxy routes if needed - Integrates i18n support - Works for both Astro and Nuxt apps Use when: Adding a new page to your frontend application. add-dashboard Add a new feature tab to the user dashboard - Creates Vue island component - Sets up Astro page shell - Adds navigation entry - Creates API proxy routes Use when: Adding a new feature/tab to the user-facing dashboard. add-provider-model Add a new AI provider model to zship-provider1-service - Parses provider API documentation - Generates complete DB seed SQL - Updates documentation - Supports KIE, WaveSpeed AI, and custom platforms Use when: Integrating a new AI provider model into the provider service. Customization Skills customize-brand Customize branding for your ZShip app - Site name, logo, colors, SEO - Theme customization - CSS variable overrides - Never modifies official templates Use when: Customizing your app's branding and visual identity. Maintenance Skills sync-upstream Sync your app with official ZShip template updates - Performs diff analysis between templates and your copy - Suggests changes to merge - Preserves your customizations - Identifies irrelevant changes to skip Use when: After pulling ZShip updates and need to merge template changes. review Review code changes for ZShip convention compliance - Checks for accidental official file modifications - Validates naming conventions - Security checklist (secrets, API keys) - Multi-tenant isolation verification - API response format validation - Architecture boundary checks Use when: Before committing code or creating pull requests. How Skills Work - Automatic Activation: When you describe a task, the AI assistant automatically identifies and uses the relevant skill. - Step-by-Step Guidance: Each skill provides detailed steps, code templates, and examples. - Convention Enforcement: Skills ensure your code follows ZShip conventions: - Never modify official files (apps/web-astro/, apps/admin/, backend/nodeX-*/) - Proper naming conventions - Multi-tenant isolation - Security best practices - Verification Checklists: Most skills include verification steps to ensure everything works correctly. Skill Structure Skills are stored as Markdown files in .cursor/skills/{skill-name}/SKILL.md with: - Frontmatter: Name and description - Critical Rules: Must-follow conventions - Step-by-Step Instructions: Detailed guidance - Code Templates: Ready-to-use code snippets - Verification: Checklists to confirm completion Best Practices - Always use skills for common tasks instead of doing them manually - Read the critical rules in each skill before starting - Follow the verification checklist after completing a task - Run review skill before committing significant changes - Use sync-upstream regularly to stay updated with template improvements Next Steps - Codebase Structure — Understand the project structure - Deployment Guide — Deploy your application - Customization Guide — Customize your app ### AI Provider Service URL: https://zship.ai/docs/value-added/ai-provider Unified KIE & WaveSpeed AI gateway with testing, task logs, analytics, public gallery, and security controls. What this service is for The AI Provider Service (zship-provider1-service) exists to solve two problems: - Integration: Beyond core node10-ai, it unifies external image / video / TTS APIs (KIE, WaveSpeed, …) behind one gateway — same auth, credits, and task model — so your product code does not sprawl with multiple SDKs and signing styles. - Operations: Async generation (callbacks, retries, refunds) is hard to run without admin tools. Testing, task logs, analytics, gallery, rate limits, and blocks are there so you can validate config, answer support, read usage, and enforce policy without shipping app releases for every change. Supported providers Provider API Typical capabilities KIE api.kie.ai Image (nano-banana-2, Seedream), video (Kling, Wan), TTS (ElevenLabs), and more WaveSpeed AI api.wavespeed.ai Image (nano-banana-2), video (Wan 2.6), editing, LoRA, and more Why: One place to manage multiple vendors — swap models or vendors mostly by configuration, not by branching product code. Core gateway capabilities Capability Why we built it No-code onboarding Add models, change mapping or validation without a new web/admin release — shorter iteration and safer rollbacks when a model is misbehaving. Credits Tied to node1-auth so every task has traceable spend — easier refunds, reconciliation, and disputes. Webhooks & R2 Most providers are async; standard callbacks and file offload let dashboards and lists show stable results instead of every client inventing URLs. Config-driven mapping Absorb vendor-specific JSON (mapping, templates, strip lists) inside the gateway so the app sends business fields only. Testing (dedicated Provider test page) Tool Why we built it Dry run Validate payload, mapping, validation, and credit estimate with no upstream call and no charge — catch misconfiguration before it burns credits in production. Live test Prove API keys, callback base URL, and connectivity with real HTTP; does not debit end-user credits — safe for destructive debugging. Status lookup Async jobs often stall in IN_PROGRESS; fetch by task id to see whether the bottleneck is upstream slowness, lost webhooks, or mapping bugs. Webhook debugging Wrong callback URLs look like “tasks never finish”; dedicated checks cut callback integration time sharply. Takeaway: Separate “is the config correct?” from “are we charging real users while experimenting?” Task & log management (Providers → Tasks) Tool Why we built it Searchable list Support can find a run by email, task id, model, … without grepping raw application logs. Per-task detail (stored requests/responses) Keeps user payload, upstream request/response, errors, and credits for disputes, audits, and engineering repro (sensitive fields may be redacted by role). Retry Transient upstream/network failures are cheaper to replay the same request than asking users to click again; also verifies “after fixing config, does the same job succeed?”. Takeaway: Observability for async pipelines — without it you only see “failed”, not where it failed. Statistics (inside Tasks) View Why we built it Overview Totals, success/fail/in-flight, credits, distinct users — capacity and cost at a glance. By provider See which vendor is expensive, slow, or flaky — data for model choices and targeted throttling. Filter by app_key Split usage by project or tenant for billing, limits, and account reviews. Takeaway: Move from “it runs” to “we know how it runs and where money goes”. Public gallery (Providers → Gallery + public GET /gallery) Tool Why we built it User opt-in public Separates shareable outputs from private generations — fewer privacy incidents. Public list API Marketing / campaign pages can show curated results in a grid without exposing R2 as a flat public file dump. Moderation & featured Community surfaces need policy review and editorial slots for campaigns and social proof. Takeaway: Turn raw task output into marketable, community-facing assets. Security & abuse prevention Control Why we built it Rate limits (scoped) Protect upstream quotas and your credit economy from scripted abuse; per-key / per-provider / global scopes with 429 and retry hints. API key blocks When keys are stolen or misused, revoke at the gateway (per provider or wildcard) faster than code deploys. IP & edge (Cloudflare) The service enforces who uses which key; IP/geo/bot rules belong on Cloudflare WAF / custom rules and stack with the controls above. Takeaway: Keep an open API usable while bounding cost and abuse risk. Content moderation setup Content moderation runs before a generation request is sent to KIE, WaveSpeed, or another generation provider. It checks only the configured request fields and normalizes the upstream response to allow, deny, flag, error, or skipped. Before you enable it - Deploy versions of zship-provider1-service and Admin that include content moderation. - Run pnpm db:migrate:remote from backend/zship-provider1-service. Confirm that 0015_content_moderation.sql and 0016_content_moderation_field_rules.sql have been applied. - Create a dedicated API key in the moderation vendor's console. Do not reuse a generation-provider key unless the vendor explicitly issues the same credential, and never put the key in frontend code or documentation. Recommended configuration sequence - Open Admin → Providers → Content moderation. - Select the project scope. All projects (default) is the wildcard fallback; an enabled configuration for a specific app_key takes precedence. - Select the moderation provider, verify the prefilled HTTPS endpoint, and enter the API key and timeout. Keep moderation disabled while testing. - Configure the default prompt fields and any provider/model-specific overrides. - Choose whether to charge the user, then configure failure and flag behavior. - Save the configuration. In the Provider test panel, test both clearly safe and clearly disallowed prompts. Tests call the real moderation API but do not debit end-user credits. - Enable moderation only after the tests pass, then monitor decisions, latency, status codes, and credits under Recent decisions. Supported moderation providers Provider Default API endpoint Authentication and behavior Creem Screen Prompt https://api.creem.io/v1/moderation/prompt Uses a Creem API key and returns a synchronous result. WaveSpeed Content Moderator https://api.wavespeed.ai/api/v3/wavespeed-ai/content-moderator/text Uses a WaveSpeed API key; the gateway handles submission, polling, and output retrieval. Keep the default endpoint unless the vendor explicitly requires another production endpoint. Endpoints must use HTTPS, and the timeout must be between 1,000 and 30,000 ms. Choose which fields to review Default prompt fields are comma-separated field paths, for example: prompt, text, negative_prompt, input.prompt Dots address nested object properties. String values are included directly; arrays of strings are included item by item, with duplicate text removed. If none of the configured fields are present, the result is skipped: no moderation request is sent and no moderation credits are charged. When generation providers or models use different request shapes, add a provider/model field override. Field selection uses this precedence: - Exact generation provider + model rule - Generation-provider default rule (model left empty) - Default prompt fields For example, if one KIE model reads input.prompt while a WaveSpeed model reads prompt, configure separate rules instead of sending both fields unconditionally. Decisions, failures, and billing Setting / result Runtime behavior allow Passes moderation and continues to the generation request. deny Always blocks generation with CONTENT_MODERATION_BLOCKED. flag Blocks when Block flagged prompts is enabled; otherwise generation continues. error Continues when Fail open is enabled; otherwise returns CONTENT_MODERATION_UNAVAILABLE. skipped No configured field was found, so moderation is not called and generation continues. Charge user When a moderation API call was attempted, charges the configured credits per check. Admin tests never debit user credits. Moderation and generation are billed separately. Once a moderation call has occurred, its credits are retained even if the prompt is blocked or the later generation fails. Only generation credits follow the normal generation-refund path. This separation applies to prepaid and enterprise billing. Before enabling user charges, verify the vendor's per-call price, your credit conversion, and the desired insufficient-balance experience. Audit and privacy boundary Recent decisions store operational metadata such as request ID, project, user, generation provider, moderation provider, decision, reason, prompt length, status code, duration, and credits. The prompt text itself is not stored. The selected text is still sent to the configured third-party moderation provider, so review the vendor's privacy, data residency, and retention terms before production use. Troubleshooting - Save before testing: the test panel uses the API key, endpoint, and timeout stored in the database. - API key required when enabling: enter a valid key and save. Existing keys may appear masked in read-only or redacted views. - A model checks the wrong fields: confirm that the generation provider key and model ID exactly match Providers configuration, and look for a higher-priority exact rule. - Many results are skipped: the configured paths do not match the actual generation request body. - The moderation vendor is intermittently unavailable: inspect response codes and latency, adjust the timeout, and decide whether fail-open matches your risk policy. - The user was charged but generation failed: this is expected for a completed moderation attempt; generation credits follow their own refund path while moderation credits are retained. Purchase & setup Contact us to purchase the AI Provider Service. After purchase: - Deploy backend/zship-provider1-service to Cloudflare - Point admin’s PROVIDER1_SERVICE binding at your Worker - Configure providers (KIE, WaveSpeed, …) under Admin → Providers Admin entry points - Providers (/providers) — config, content moderation, tasks, stats, gallery, rate limits, blocks - Provider test (/providers-test) — dry run, live test, status, webhook checks ### AI Provider Turnstile URL: https://zship.ai/docs/value-added/ai-provider-turnstile Project-level Cloudflare Turnstile protection for AI Provider generation. What it protects AI Provider Turnstile adds a Cloudflare Turnstile challenge before users can start zship-provider1-service generation from the shared AI UI. It applies to the common Nuxt AI layer, including: - AiPlayground - AiGeneratorStudio - Any app that reuses the shared /api/provider/generate flow The goal is to reduce scripted abuse of image, video, and other paid generation endpoints without adding per-app code. Admin configuration Open Admin → Projects → your project → AI Provider Turnstile. Configure: Setting Meaning Enable Turnstile Turns the challenge on for this app_key. Site key Public Cloudflare Turnstile site key used by the browser widget. Secret key Private Cloudflare Turnstile secret key stored in t_project.settings; it is never returned to the browser. Challenge scope Choose Guests only or All users. Allow subscription bypass Lets only selected active subscription plans skip the challenge. Subscription plans that can bypass Multi-select one or more subscription Price Config entries. The stored value is price_type, which is also the user's t_user_subscription.plan_id. Settings are stored under settings.ai_provider_turnstile on the project record. No per-app project config file is required. Cloudflare setup In Cloudflare Dashboard: - Open Turnstile. - Create a widget. - Choose Managed widget mode. - Add your production domain and any preview domains. - Copy the Site Key and Secret Key into the Project settings in Admin. Cloudflare's server verification contract is the Turnstile Siteverify API. Tokens are short-lived and single-use, so the UI resets the widget after each generation attempt. Runtime flow - The Nuxt app fetches /api/site-config/ai-provider-turnstile. - The common site config proxy asks node1-auth for the current project's Turnstile policy. - node1-auth returns a secret-redacted config: whether Turnstile is enabled, whether this visitor must solve it, the site key, scope, and subscription-plan bypass rule. - When required, the shared AI component renders the Turnstile widget and sends turnstile_token with the generate request. - zship-provider1-service validates the normal request first, then asks node1-auth to verify the token against Cloudflare Siteverify. - Only verified requests continue to credit deduction and provider execution. The token is stripped from provider requests and from stored task user_request records. Recommended defaults For most public AI generation products: - Use Guests only first. - Enable subscription bypass only for paid tiers that should have lower friction. - Combine Turnstile with provider rate limits and API key blocks. - Use Cloudflare WAF or custom rules for IP, country, or bot-score policy. This keeps friction low for paying users while still protecting free guest traffic and expensive upstream models. ### Value-Added Services URL: https://zship.ai/docs/value-added/overview Optional services: AI Provider. Purchasable separately. Value-added services are not included in core deployment. They can be purchased and configured separately. Available services Service Description Purchase AI Provider Service Unified KIE & WaveSpeed AI gateway; testing, task logs, analytics, public gallery, and rate limits / API blocks (pair with Cloudflare WAF for IP rules). AI Provider Service Core vs value-added - Core: apps/web-astro, apps/admin, node1~node11 — deployed by default - Value-added: zship-provider1-service — optional, purchasable or provided under contract Admin binding After purchasing AI Provider Service, configure the PROVIDER1_SERVICE binding in admin's wrangler to point to your deployed zship-provider1 Worker. ## Blog ### Building a product docs system that scales past launch URL: https://zship.ai/blog/building-a-product-docs-system A practical look at how ZShip connects documentation, releases, pricing, and customer delivery into one product surface. ## Documentation is part of the product The best docs do more than explain setup. They reduce support load, clarify the buying path, and give customers confidence that the system is maintained. For a SaaS foundation like ZShip, that means docs should sit close to pricing, changelog, downloads, and support. Customers should never feel like they crossed into a separate website just to answer a basic question. ## Keep the hierarchy small A Mintlify-style documentation surface works because the visual system is quiet: - Side navigation stays compact - Article titles are large but not heavy - Body text is readable without becoming editorial - Code, tables, and callouts have clear boundaries That structure lets technical content feel calm even when the implementation underneath is deep. ## Treat release content as navigation Docs, changelog, and blog posts should point to each other. A release note can link to the relevant guide. A blog post can explain why a workflow exists. A pricing page can link back to the docs that describe delivery. The result is a product surface that feels coherent instead of assembled from separate templates. ### Pricing pages for technical products should be direct URL: https://zship.ai/blog/pricing-pages-for-technical-products How to make pricing feel transparent when plans, credits, and checkout are driven by backend configuration. ## Buyers want confidence, not decoration Technical buyers scan pricing pages for the answer to a simple question: what do I get, what does it cost, and what happens after payment? That is why the best pricing pages keep type hierarchy simple. The headline names the page. The plan cards explain the offer. Supporting FAQ content handles edge cases without fighting for attention. ## Backend-driven plans need stable presentation When plans come from a payment service, the frontend should avoid hardcoded assumptions. Labels, credits, intervals, badges, and features can change without a deploy. The UI still needs a strong frame: - Consistent plan card spacing - Clear price and interval hierarchy - Feature lists that align across cards - Checkout buttons that do not shift layout ## Make the next step obvious Once payment succeeds, users need delivery. In ZShip, pricing connects directly to checkout confirmation, GitHub access, downloads, and support. That continuity matters more than adding another decorative section. ### Why customer platforms still need a blog URL: https://zship.ai/blog/why-customer-platforms-need-a-blog Blogs are useful when they explain decisions, teach workflows, and create durable entry points into the product. ## A blog is a product education layer The strongest product blogs are not announcement dumps. They explain how the product thinks, what tradeoffs matter, and how users should make decisions. For ZShip, blog content can cover launch workflows, deployment choices, pricing patterns, and operational lessons. Those posts become durable paths into the docs and pricing pages. ## Keep the writing interface calm Blog typography should share the same system as docs: - Moderately large titles - Comfortable paragraph rhythm - Lightweight metadata - Simple inline links - Code blocks that match documentation That consistency makes the site feel like one product, even when a visitor moves between docs, pricing, and articles. ## Store Assets ### Customer Dashboard Starter URL: https://zship.ai/store/customer-dashboard-starter A dashboard surface for customer accounts, orders, licenses, downloads, support, credits, and delivery status in a ZShip product. Category: template. Framework: Nuxt. Delivery: snippet. ### Cloudflare Worker Service Map URL: https://zship.ai/store/cloudflare-worker-service-map A reference block for documenting auth, pay, support, blog, CDN, site, AI, prompt, check-in, and delivery Worker boundaries. Category: block. Framework: Markdown. Delivery: snippet. ### SEO and GEO Launch Checklist URL: https://zship.ai/store/seo-geo-launch-checklist A practical checklist for public pages, structured data, sitemap coverage, robots policy, llms.txt, answer blocks, and comparison content. Category: snippet. Framework: Markdown. Delivery: snippet. ## Agent Resources - MCP endpoint: https://zship.ai/mcp - Agent manifest: https://zship.ai/.well-known/ai-agent.json - Sitemap: https://zship.ai/sitemap.xml The MCP server exposes read-only public knowledge tools and never exposes accounts, checkout state, private support data, or credentials.