Client Hosting Guide
Onboard a new client tenant onto a shared Collective Vision deployment.
How to onboard a new client entity onto Collective Vision hosted on our Cloudflare account.
Architecture overview
Client's Website Our Cloudflare Account
┌─────────────────┐ ┌──────────────────────────────┐
│ │ │ │
│ script src= │───────────────▶│ Cloudflare Worker │
│ "feedback. │ │ (collective-vision-feedback) │
│ client.com/ │ │ │ │
│ widget.js" │ │ ▼ │
│ │ │ Cloudflare D1 Database │
│ │ │ ┌──────────────────────┐ │
│ │ │ │ workspace: acme-corp │ │
│ │ │ │ (isolated by │ │
│ │ │ │ workspace_id FK) │ │
│ │ │ └──────────────────────┘ │
└─────────────────┘ └──────────────────────────────┘
▲
│
┌──────────┴──────────┐
│ Admin UI │
│ (Cloudflare Pages) │
│ admin.ourdomain.com │
└─────────────────────┘Key points:
- One Worker serves all tenants; data is isolated by
workspace_idin D1 - Each client gets a unique workspace slug and admin token
- Clients can use our shared domain or CNAME their own subdomain
- The widget auto-detects its API base from its own
<script>src URL
Onboarding checklist
1. Provision workspace
Choose a workspace slug for the client. Convention: lowercase, hyphenated company name.
Example: acme-corp, startup-io, bigco-feedbackNo manual creation needed — the workspace auto-provisions on first API call. But you can pre-seed it:
curl -X POST "https://feedback.ourdomain.com/api/v1/acme-corp/main/feedback" \
-H "Content-Type: application/json" \
-d '{"title":"Welcome","description":"Your feedback board is ready!","externalUserId":"setup"}'2. Generate admin token
Generate a secure random token (32+ characters):
openssl rand -hex 32
# Example output: a1b2c3d4e5f6...Store it as a Cloudflare secret (if using per-client tokens) or document it in
your internal records. The current implementation uses a single
ADMIN_API_TOKEN for all workspaces — for per-client tokens, a future update
will add workspace-scoped auth.
3. Configure CORS
Add the client's domain to ALLOWED_ORIGINS in the appropriate environment.
In wrangler.toml (for the deployed environment):
[env.production.vars]
ALLOWED_ORIGINS = "https://feedback.ourdomain.com, https://www.acmecorp.com, https://acmecorp.com"Wildcards are supported: https://*.acmecorp.com
After updating, redeploy: wrangler deploy --env production
4. Custom domain (optional)
If the client wants feedback.clientdomain.com instead of using our shared
domain, see DNS Setup for full instructions.
Quick version:
- Client creates CNAME:
feedback.clientdomain.com→feedback.ourdomain.com - We add a Worker route for their domain in
wrangler.toml - We add their domain to
ALLOWED_ORIGINS - Redeploy
5. Generate widget embed code
Provide the client with their embed snippet:
<!-- Using our shared domain -->
<script
src="https://feedback.ourdomain.com/widget.js"
data-workspace="acme-corp"
data-board="main"
></script>Or with a custom domain:
<!-- Using client's custom domain -->
<script
src="https://feedback.acmecorp.com/widget.js"
data-workspace="acme-corp"
data-board="main"
></script>Optional attributes:
data-board="feature-requests"— target a specific boarddata-accent-color="#3b82f6"— customize widget accent color
6. Admin UI access
Option A: Shared admin portal
- Client logs in at
https://admin.ourdomain.com - They enter their workspace slug and admin token
- The admin UI scopes all queries to their workspace
Option B: Dedicated admin build
- Build the admin UI with their API URL:
cd admin VITE_API_URL=https://feedback.acmecorp.com npm run build - Deploy to Cloudflare Pages under a custom domain (e.g.,
admin.acmecorp.com)
7. Seed demo data (optional)
To give the client a pre-populated demo:
./scripts/seed-demo.sh https://feedback.ourdomain.com their-admin-tokenThis creates 20 feedback items, 6 tags, 80+ votes, and 10 comments.
8. Verify setup
Run through this checklist:
-
curl https://feedback.ourdomain.com/healthreturns{"ok":true} - Widget loads on client's site without CORS errors
- Feedback submission creates item in correct workspace
- Voting works (and deduplicates correctly)
- Admin login succeeds with provided token
- Admin dashboard shows workspace stats
- Custom domain resolves (if applicable)
Client deliverables
Send the following to the client:
| Item | Value |
|---|---|
| Workspace slug | acme-corp |
| Admin token | (deliver securely — encrypted email, password manager, or 1:1 channel) |
| Widget embed code | a <script> tag with data-workspace="acme-corp" and data-board="main" |
| Admin dashboard URL | https://admin.ourdomain.com |
| API base URL | https://feedback.ourdomain.com/api/v1/acme-corp/main/feedback |
| Widget customization | Board slugs, accent color, additional boards |
Multi-board setup
Clients can organize feedback across multiple boards. Boards auto-provision when feedback is first submitted to a new board slug.
Common patterns:
main— General product feedbackfeature-requests— Feature ideas and requestsbugs— Bug reportsinternal— Internal team feedback (setis_public = 0via admin)
Each board gets its own widget embed code with a different data-board value.
Billing considerations
Track per-workspace usage for billing:
-- Feedback count per workspace
SELECT w.slug, COUNT(f.id) as feedback_count
FROM workspaces w
JOIN boards b ON b.workspace_id = w.id
JOIN feedback_items f ON f.board_id = b.id
GROUP BY w.id;
-- Vote count per workspace
SELECT w.slug, COUNT(v.id) as vote_count
FROM workspaces w
JOIN boards b ON b.workspace_id = w.id
JOIN feedback_items f ON f.board_id = b.id
JOIN feedback_votes v ON v.feedback_id = f.id
GROUP BY w.id;Deprovisioning a client
To remove a client's data:
- Delete all feedback items, votes, comments for the workspace
- Delete boards and end_users for the workspace
- Delete the workspace record
- Remove their domain from
ALLOWED_ORIGINSand Worker routes - Redeploy
There is no automated deprovision script yet — this should be done via D1 SQL queries with care.