Collective Vision

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_id in 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-feedback

No 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:

  1. Client creates CNAME: feedback.clientdomain.comfeedback.ourdomain.com
  2. We add a Worker route for their domain in wrangler.toml
  3. We add their domain to ALLOWED_ORIGINS
  4. 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 board
  • data-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-token

This creates 20 feedback items, 6 tags, 80+ votes, and 10 comments.

8. Verify setup

Run through this checklist:

  • curl https://feedback.ourdomain.com/health returns {"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:

ItemValue
Workspace slugacme-corp
Admin token(deliver securely — encrypted email, password manager, or 1:1 channel)
Widget embed codea <script> tag with data-workspace="acme-corp" and data-board="main"
Admin dashboard URLhttps://admin.ourdomain.com
API base URLhttps://feedback.ourdomain.com/api/v1/acme-corp/main/feedback
Widget customizationBoard 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 feedback
  • feature-requests — Feature ideas and requests
  • bugs — Bug reports
  • internal — Internal team feedback (set is_public = 0 via 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:

  1. Delete all feedback items, votes, comments for the workspace
  2. Delete boards and end_users for the workspace
  3. Delete the workspace record
  4. Remove their domain from ALLOWED_ORIGINS and Worker routes
  5. Redeploy

There is no automated deprovision script yet — this should be done via D1 SQL queries with care.

On this page