A runnable, end-to-end prototype of the system described in DESIGN.md:
a hosted backend (ingest + admin API), an embeddable widget, a JS SDK, and a tenant dashboard.
jicama is a drop-in feedback plugin. Get a key, paste one tag, done — a themed feedback button shows up and submissions land in your private dashboard.
-
Get your key — open the landing page → Get Started → sign in with Google. You get a public key (
pk_…) instantly. -
Embed it — drop this into your site's HTML (point
data-apiat your jicama backend):<script src="https://YOUR-BACKEND/frontend/widget/feedback.js" data-key="pk_your_key" data-api="https://YOUR-BACKEND"></script>
-
See feedback — open your dashboard. That's it. Theme the widget, lock it to your domain, and buy token packs from the dashboard as you grow.
Pitching it / linking from your company site: point a "Feedback" or "Get Started" button at your hosted landing page (
https://YOUR-BACKEND/), and add the<script>above to start collecting. Prefer code? The whole thing is open on GitHub — clone,npm run dev, and it's live locally in one command (see Run it).
Prototype scope: data persists in SQLite via Prisma (a single
dev.dbfile), seeded with a demo tenant — so it runs with zero external services and survives restarts. The schema matches DESIGN.md §6 exactly; going to Postgres is a one-line change (provider = "postgresql"inprisma/schema.prisma+ a PostgresDATABASE_URL). All DB access is behindbackend/src/store.ts, so the widget, SDK, and dashboard never change.
cd backend
cp .env.example .env # SQLite path + port; no secrets, safe defaults
npm install
npm run dev # auto-runs `prisma generate` + `prisma db push`, seeds, then startsThe first npm install generates the Prisma client; npm run dev/start create the SQLite
schema and seed the demo tenant automatically. No manual DB steps.
Then open:
| URL | What |
|---|---|
| http://localhost:4000/ | Landing page — what jicama is, how to integrate, live pricing; Get Started → login |
| http://localhost:4000/login | Login — "Continue with Google" → onboarding → your dashboard |
| http://localhost:4000/signup | Pricing + self-serve signup — pick a plan & pay (test mode), or Continue with Google for instant onboarding |
| http://localhost:4000/auth/google | Simulated "Sign in with Google" → onboarding form → personalized dashboard |
| http://localhost:4000/dashboard | The tenant admin console (React + Vite + Tailwind) — Feedback inbox, Widget (theming), Billing, Settings tabs |
| http://localhost:4000/admin | Super Admin (platform owner) — email/password login → edit token pricing + view all orgs |
| http://localhost:4000/health | Health check |
Super Admin is the platform-owner role (you), separate from tenant logins. Default credentials are printed on boot and set via
SUPERADMIN_EMAIL/SUPERADMIN_PASSWORD(defaultssuper@jicama.tech/jicama-super-2026). The panel edits token-pack pricing (stored in the DB, applied live to every tenant's Billing tab) and lists all organizations with their plan, token balance and feedback count. Password is scrypt-hashed.
Sign-in is a simulated Google login (test mode) — no Google Cloud setup, works offline. Clicking "Sign in with Google" creates a user + an httpOnly session cookie; first-timers complete a short onboarding form that creates their org, then land on their own dashboard (no key-pasting). The admin API accepts either that session or a secret key, so the legacy
?key=sk_…flow still works. Real Google OAuth is built in and switches on automatically when you setGOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET(see Going live) — otherwise the simulated login is used.
Billing is token-based, in simulated "test mode" — no real charges. Tokens are the currency: every accepted feedback spends 1 token, and orgs buy token packs (Starter 1k/$9, Growth 10k/$79, Scale 100k/$599) from the dashboard's Billing tab to top up. New orgs get a free starter grant. In the simulated fallback, use card
4242 4242 4242 4242(any future expiry + CVC) to succeed, or4000 0000 0000 0002to see a decline. Real Stripe Checkout is built in and switches on when you setSTRIPE_SECRET_KEY(see Going live).
The dashboard at
/dashboardis the prebuilt React app (frontend/dashboard/dist) and is served by the backend, so the singlenpm run devabove is enough to use it. If/dashboardshows a "not built yet" hint, build it once:cd frontend/dashboard && npm install && npm run buildFor hot-reload while developing the dashboard, run it on its own Vite server instead — it proxies
/v1to the backend, so keep the backend running too:cd frontend/dashboard && npm run dev # → http://localhost:5173/dashboard/
Two demo orgs are seeded (printed on boot) to show multi-tenant isolation — each org's dashboard only ever sees its own feedback:
Acme Inc. (pro) public pk_demo_acme_123 secret sk_demo_acme_456
Globex Corp. (free) public pk_demo_globex_789 secret sk_demo_globex_012
In the dashboard, switch orgs by pasting another secret key (top-right "API key"), or deep-link
straight in with …/dashboard/?key=<secret> (the signup page does this automatically).
With the server running, in another terminal:
cd backend
npm run smokeIt runs 110 checks across the whole system (in the zero-setup fallback mode) — ingest, auth/trust separation, validation, spam honeypot, admin stats/list/filter/patch, signup, simulated payments (incl. declined cards), token balance + spend-per-feedback + buying token packs, multi-tenant isolation, per-project origin lock-down, widget theming/branding, simulated Google login → onboarding → session-based dashboard, and that the widget + signup + React dashboard are served — printing ✅/❌ per check and exiting non-zero on any failure. Expected:
✅ ALL PASS — 110 passed, 0 failed
1. Zero-code <script> — one tag, a floating button appears:
<script src="https://cdn.jicama.tech/feedback.js"
data-key="pk_demo_acme_123"
data-color="#6C2BD9"></script>The widget pulls its theme/branding from the server (
GET /v1/config), so brand color, dialog background, button text, modal copy and white-labeling are all controlled from the dashboard's Widget tab — change them there and every embed updates, no code edit.data-*attributes act as the initial look until the saved theme loads.
2. SDK (frontend/sdk) — full control + user context:
import { Feedback } from '@jicama/feedback';
Feedback.init({ key: 'pk_demo_acme_123', user: { id: 'u_1', email: 'jane@acme.example' } });
Feedback.open(); // open the widget UI
await Feedback.submit({ type: 'bug', message: 'Crash on save', rating: 2 }); // headless3. REST — any platform (mobile, desktop, CLI, backend):
curl -X POST http://localhost:4000/v1/feedback \
-H "Authorization: Bearer pk_demo_acme_123" \
-H "Content-Type: application/json" \
-d '{"type":"idea","message":"Add CSV export","rating":5,
"metadata":{"appVersion":"2.1.0","os":"iOS 17"}}'Drop the widget into any local app or site you're building and see the feedback land in your dashboard only — each org's keys are fully isolated from every other org's.
1. Run the backend (it's the API + dashboard host):
cd backend && cp .env.example .env && npm install && npm run dev # → http://localhost:40002. Get your own keys. Open http://localhost:4000/signup, create an org (the free plan
needs no card). You'll get a public key (pk_…, for the widget) and a secret key
(sk_…, for your dashboard). Or just reuse the seeded demo keys pk_demo_acme_123 /
sk_demo_acme_456.
3. Embed the widget in your project's HTML. Because your app runs on a different port than
the backend, point data-api at the backend — CORS is open, so cross-origin works:
<script src="http://localhost:4000/frontend/widget/feedback.js"
data-key="pk_YOUR_PUBLIC_KEY"
data-api="http://localhost:4000"
data-color="#6C2BD9"></script>A floating 💬 button appears. (When the page and the API share an origin, data-api can be
omitted — across projects/origins you must set it.)
4. View feedback as the admin. Open http://localhost:4000/dashboard, paste your sk_…
(or deep-link …/dashboard/?key=sk_…). You'll see only your org's submissions, live.
Status: fully working locally; the main production-hardening items are now built in. API keys are indexed by a SHA-256 hash for constant-time lookup, attachment storage is pluggable (inline → filesystem → S3/R2), the rate limiter is behind a swappable store (in-memory → Redis), and origin allow-lists, real Google OAuth, and real Stripe Checkout are all available (see below). What's left for a real deployment is mostly ops: a managed Postgres, an S3/R2 bucket, Redis, and the backend behind HTTPS. See
DESIGN.md§10 and Production hardening below.
Both production integrations are opt-in via env vars — set them and they switch on; leave
them blank and the app uses the simulated login / simulated charge (zero setup). Both work with
free test-mode credentials. Copy the keys into backend/.env (see backend/.env.example).
Real "Sign in with Google":
- In Google Cloud Console → APIs & Services → Credentials, create an OAuth client ID of type Web application.
- Add an Authorized redirect URI:
http://localhost:4000/auth/google/callback. - Put the values in
backend/.env:Restart the backend —APP_URL="http://localhost:4000" GOOGLE_CLIENT_ID="…apps.googleusercontent.com" GOOGLE_CLIENT_SECRET="…"
/auth/googlenow redirects to the real Google consent screen, exchanges the code, and creates the session. No code change. (Implementation:backend/src/auth/google.ts.)
Real Stripe Checkout (for buying token packs):
- In the Stripe Dashboard (test mode), copy your
Secret key (
sk_test_…). - Put it in
backend/.env:Restart the backend. Now Buy in the dashboard's Billing tab redirects to Stripe's hosted checkout page; on success the backend verifies the session and credits the tokens (idempotently via the Checkout session id). Pay with Stripe's test cardSTRIPE_SECRET_KEY="sk_test_…"4242 4242 4242 4242. (Implementation:backend/src/billing/stripe.ts+ the/v1/billing/checkout/returnendpoint.)
The 66-check smoke test runs against the fallback (no keys) path, so it stays green with zero setup; the real paths activate only when the env vars are present.
The security/architecture hardening is built in and opt-in via env where it needs external services, so the zero-setup prototype keeps working:
| Concern | Prototype default | Production switch |
|---|---|---|
| API key storage | keys are viewable in the dashboard (Settings → Your API keys) for easy integration — the "test-mode keys" UX | Keys are indexed by a SHA-256 hash (keyHash) for constant-time lookup. For a hardened live mode, store the secret encrypted (or show it once + offer rotate) instead of plaintext. (backend/src/store.ts hashKey/getTenantKeys) |
| Attachments | inline data-URLs in the DB |
Set STORAGE=filesystem to write blobs to ./uploads and store a /uploads/… URL instead. Adding S3/R2 is one put() branch in backend/src/storage.ts — nothing upstream changes. |
| Rate limiter | in-memory store | Implement the RedisStore in backend/src/middleware/rateLimit.ts and set REDIS_URL for multi-instance counting. The middleware is already store-agnostic. |
| Database | SQLite (dev.db) |
Set provider = "postgresql" in prisma/schema.prisma, point DATABASE_URL at Postgres, npx prisma db push. All DB access is behind store.ts; the rest of the app is unchanged. (On Postgres you'd also switch the JSON-encoded String columns to native Json/enums.) |
Existing keys in an older
dev.dbget their hash backfilled automatically on boot. The smoke test covers key-authed access, the dashboard keys endpoint, and an attachment round-trip.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /v1/feedback |
public key | Submit feedback (ingest). Spends 1 token; returns 402 when the org is out of tokens |
| GET | /v1/config |
public key | Widget pulls project theme |
| GET | /v1/plans |
— | Plan catalogue (free/pro/enterprise) for the pricing page |
| POST | /v1/signup |
— | Self-serve org signup → creates an isolated tenant + keys (charges card for paid plans) |
| GET | /auth/google |
— | Start sign-in → redirects to real Google (if configured) or the mock page |
| GET/POST | /auth/google/callback |
— | OAuth code exchange (real) / mock form post → creates the user + session cookie |
| GET | /v1/billing/checkout/return |
— | Stripe Checkout success return → verifies payment, credits tokens (idempotent) |
| POST | /auth/logout |
session | End the session |
| GET | /v1/me |
session | Current user + org (drives the dashboard & onboarding) |
| POST | /v1/onboarding |
session | Create the org for a signed-in user (first-time setup) |
| GET | /v1/admin/feedback |
secret key | List/filter feedback |
| PATCH | /v1/admin/feedback/:id |
secret key | Change status |
| GET | /v1/admin/stats |
secret key | Counts + avg rating |
| GET | /v1/admin/projects |
secret key | List projects |
| PATCH | /v1/admin/projects/:id |
secret key | Lock the public key to specific origins (or ["*"] for any) |
| GET | /v1/admin/billing |
secret key / session | Token balance, packs, card on file, invoices |
| POST | /v1/admin/billing/buy-tokens |
secret key / session | Buy a token pack (charges card → tops up balance) |
| POST | /v1/admin/billing/checkout |
secret key / session | Change plan tier (legacy; charges card) |
| GET | /v1/admin/billing/invoices |
secret key / session | Payment history |
backend/ Express + TS. ingest API, admin API, auth + rate-limit middleware.
prisma/ schema.prisma (data model §6) + seed; dev.db is the SQLite file.
src/store.ts the only file that touches the DB — swap target for Postgres.
frontend/
widget/ feedback.js (embeddable) + demo.html
sdk/ @jicama/feedback npm package (wraps API + widget)
dashboard/ React + Vite + Tailwind admin console (built to dist/, served by backend)
- ✅
Replace the in-memory store with Prisma— done (SQLite now; flip provider for Postgres). - ✅
Build the dashboard as the planned React + Vite app— done (frontend/dashboard). - ✅
Hash-index API keys— done (SHA-256 lookup; keys viewable in the dashboard, see Production hardening). - ✅
Move attachments off inline data-URLs— pluggable storage (STORAGE=filesystem; S3/R2 is oneput()branch). - ✅
Real accounts + billing— Google login (mock + real OAuth), tokens, Stripe Checkout. - ✅
Per-project origin allow-lists & widget theming— done (Settings + Widget tabs). - Provision the managed services (Postgres, Redis, S3/R2) + deploy behind HTTPS.
- Key-management endpoints (rotate/revoke) in the admin API; notifications/webhooks; team roles.