Documentation
Wippit puts your business on the web — a site, an app, the data behind it, your own domain — without servers to run or deploys to babysit. Everything is one tool, wippctl, which an AI agent or a person can drive. This is the complete tour.
# the two sources of truth — always current with your installed version wippctl describe # every command + flags, as JSON (for tooling/AI) wippctl cms model # how a site is structured (components, types, gotchas)
What Wippit is
You get a tenant on the shared Wippit platform — an isolated database schema, your own admin user, your own content. On it you can:
- Publish a site — static pages or single-page apps, multiple components per site, atomic activation, instant updates. No redeploy.
- Store & serve data — declare a table with one JSON spec and read/write it over a clean
/api/dataREST plane, with per-object access policies. - Host files & media — public assets or private files behind per-path ACLs and revocable passcode share links.
- Use your own domain — one DNS verification, then HTTPS is issued automatically on first visit.
- Run wipps — packaged process automations (forms, cards, signals) deployed with one command.
You start on a <you>.wipp.it subdomain provisioned for you, and bring your own domain whenever you’re ready. Everything below is one wippctl call — and every one of them is also an API-token gRPC call, so CI pipelines and AI agents drive the exact same surface. But you usually won’t type any of them — you just ask Claude.
No workspace yet? Start free — first month free, then $19/mo — or email hello@wippit.systems.
You don’t run these commands — Claude does
Everything on this page is a wippctl command — but you almost never type one. Wippit ships a Claude skill that teaches Claude Code the entire surface below. So you just describe what you want in plain language, and Claude builds it and publishes it live to your <you>.wipp.it, running every command for you. You never touch a server, a token, or a terminal.
Install once — this drops both the wippctl CLI and the wippit-publish skill into Claude Code:
# macOS / Linux curl -fsSL https://get.wipp.it/install.sh | sh # Windows (PowerShell) irm https://get.wipp.it/install.ps1 | iex
Then open Claude Code in any folder and just talk:
| You say… | …Claude does |
|---|---|
| “Build a landing page for my dental clinic and put it online.” | designs the site, creates a template, publishes it to clinic.wipp.it |
“Add a booking form at /book that saves to a table.” | defines a data object, builds the form app, mounts it at /book |
| “Use my logo and brand colors everywhere.” | sets your brand — applied to every page, email & report |
| “Email a branded welcome when someone signs up.” | authors an email template and wires the send |
| “Give me a monthly sales PDF grouped by region.” | builds a report over your data and generates the PDF |
Under the hood Claude is driving the exact wippctl calls documented here — it reads wippctl cms model and wippctl describe (the self-describing, always-current reference) before it builds, so it never works from a stale command set. It also snapshots before risky changes, so “undo that” just works.
No Claude Code? You can drive the same surface yourself with the CLI — the rest of this page is exactly that. New here? Start free.
Walkthroughs
Three real shapes of “just ask Claude.” You talk; Claude builds and publishes — every linked capability is something it sets up for you in the same conversation. None of these needed a developer.
Business — a dental clinic
Run-my-operation: a site, online booking, branded email, a weekly report.
| You say… | …Claude does |
|---|---|
| “Build a site for my clinic, Sonríe — services, hours, team.” | designs & publishes sonrie.wipp.it |
“Add appointment booking at /agenda that saves each request.” | a citas data object + booking app mounted at /agenda |
| “Use our turquoise brand & logo, and email patients a confirmation.” | sets the brand + an email template |
| “Every Monday, a PDF of next week’s appointments by dentist.” | a grouped report → PDF |
| “Put it on sonriedental.com.” | wires the custom domain |
Personal — a wedding site
A life event, zero tech: a site, RSVPs, a photo gallery, your own domain.
| You say… | …Claude does |
|---|---|
| “Make a wedding site for Diego & Lucía, Oct 12 — our story, venue, map.” | publishes diegoylucia.wipp.it |
| “Add an RSVP form — name, number of guests, meal choice.” | an rsvps data object + the form |
| “A photo gallery from our engagement shoot.” (drops in photos) | uploads them to public storage & builds the gallery |
| “Email us whenever someone RSVPs, and use diegoylucia.com.” | an email notify + the custom domain |
Education — an English tutor
Teach & credential: enrollment, gated materials, and a certificate PDF.
| You say… | …Claude does |
|---|---|
| “Build a site for my English classes — levels, schedule, prices.” | publishes profeana.wipp.it |
“Let students enroll at /inscribirse, and email them the Zoom link.” | an inscripciones data object + an email template |
| “My course PDFs should be downloadable only by enrolled students.” | private storage + a passcode share link |
| “Generate a completion certificate PDF for a student.” | a report (single datasource → a personalized certificate PDF) |
Same platform, same commands underneath — the difference is only what you asked for. When you’re ready to drive it yourself, the Quickstart and the sections below are that surface.
Quickstart
1. Start your workspace. Start free at account.wipp.it/join — you pick your customerName (lowercase letters/digits, 3–63 chars, permanent — it’s your subdomain) and your workspace is provisioned for you.
You get back a subdomain (acme.wipp.it), a one-time adminPassword, and usually an apiToken (wpt_…). Save them. The token can be reissued but never retrieved later.
2. Point wippctl at your tenant.
wippctl env add acme --server grpcs://wipp.it:443 --token wpt_XXXXXX_xxxx
wippctl env use acme
wippctl whoami # smoke test — should print your tenant + user
No token yet? Log in once and mint one:
echo '<one-time-password>' | wippctl login admin@acme # prompts you to set a real password wippctl token issue --name workstation # stored in your env config
3. You’re already live. Every tenant ships with a bilingual homepage template active out of the box:
curl -s -o /dev/null -w '%{http_code}\n' https://acme.wipp.it/ # 200
From here, replace that homepage with your own site (below), or jump straight to data, files, or a custom domain.
Core concepts
| Thing | What it is |
|---|---|
| Template | Your site. One is active at a time; activation is atomic. Carries a data map substituted into files. |
| Component | A directory of files inside a template, mounted at a URI prefix. Type static (files as-is) or spa (serves index.html on a sub-path miss, for client-side routers). |
| Data object | A tenant-owned table you declare with a JSON spec, read/written over /api/data with its own access policy. §Data |
| Storage | Tenant-wide files separate from any template — public (open) or private (per-path ACL + share links). §Files |
| Wipp | A packaged process automation — forms, cards, signals — deployed as a unit. §Wipps |
| Token / role | API tokens authenticate agents & CI; roles and groups gate who can read/write what. |
The throughline: you declare intent, the server reconciles it. Re-defining a template, object, or component is idempotent — safe to run again, safe for an AI to generate.
Publish a site
A template holds components. Conventional layout: base → /assets, root → /, and an errors component (no mount — invoked by status code).
wippctl cms template create site --display-name "Acme" \
--data '{"site_name":"Acme","primary_color":"#0066cc","tagline":"Plumbing since 1842"}'
wippctl cms component add site/base --uri-mount /assets --type static
wippctl cms component add site/root --uri-mount / --type static
wippctl cms component add site/errors --uri-mount "" --type static
wippctl cms file write site/root/index.html --from ./index.html
wippctl cms file write site/base/site.css --from ./site.css
wippctl cms file write site/errors/404.html --from ./404.html
# activate — only one template is active at a time
wippctl cms template activate site
# if the default homepage is still active, pass its etag:
# wippctl cms template list --json | jq -r '.templates[]|select(.name=="site").etag'
# wippctl cms template activate site --expected-etag <etag>
Inside any text file, Wippit, , etc. are substituted server-side at upload. Edit the template’s data later and components re-substitute automatically.
Add an app at a sub-path
An SPA is a spa component — on a URI miss within its mount it serves index.html, so client-side routers work. The one gotcha: build it with its base set to the mount, or assets 404.
# build with the mount as base — Vite: vite build --base=/cotizacion/ wippctl cms component add site/cotizacion --uri-mount /cotizacion --type spa wippctl cms file write site/cotizacion/index.html --from ./dist/index.html wippctl cms template reload site # publishes the new component, live — no redeploy
Adding or removing a component — then cms template reload — takes effect immediately on your live site. Each feature you ship is just another sub-path component on the same tenant.
The data API
A data object is a tenant-owned table you declare with a JSON spec. The server creates (or additively extends) the physical table and exposes it at /api/data/<object>. Re-defining is idempotent and never destructive — columns are only ever added, never dropped, and incompatible type changes are rejected.
1. Define the object. Columns use logical types (text, number, integer, boolean, timestamp, json) — never raw SQL types. id and created_at are added for you.
cat > post.json <<'JSON'
{
"object": "post",
"columns": [
{"name": "title", "type": "text"},
{"name": "body", "type": "text"},
{"name": "author", "type": "text", "nullable": true},
{"name": "views", "type": "integer", "nullable": true}
],
"read_policy": "public",
"write_policy": "authenticated"
}
JSON
wippctl data define -f post.json
wippctl data define -f post.json --dry-run # validate + report changes, apply nothing
Access policies gate reads and writes independently. Each is public, authenticated, a selector like {"role":"customer"} or {"group":"editors"}, or empty "" which means deny (the safe default).
2. Write and read rows from the CLI:
echo '{"title":"Hello","body":"First post","author":"alice"}' | wippctl data insert post -f -
wippctl data ls
wippctl data describe post
wippctl data query post --where 'author=alice' --sort created_at --desc --limit 20
wippctl data aggregate post --fn count
wippctl data aggregate post --fn sum --column views --where 'author=alice'
Query filters are --where 'col=val' (equals) or --where 'col.op=val' where op is one of ne lt lte gt gte like ilike in; repeatable. Values bind as parameters and the column is validated against the declared schema — the self-documenting query is the injection-safe query.
3. Read it from the browser. Anything you can do over the CLI is a REST call your site’s JavaScript can make directly (subject to the object’s policies):
| Verb | Path | Does |
|---|---|---|
GET | /api/data/{object} | List rows. Query: ?limit&offset&sort&dir&<col>=<val> |
GET | /api/data/{object}/{id} | One row by id |
GET | /api/data/{object}/aggregate | ?fn=count|sum|avg|min|max&column=… |
POST | /api/data/{object} | Insert — body {"fields": {…}} |
PUT | /api/data/{object}/{id} | Update — body {"fields": {…}} |
DELETE | /api/data/{object}/{id} | Delete a row |
# public read, straight from your front-end: fetch('https://acme.wipp.it/api/data/post?sort=created_at&dir=desc&limit=10') .then(r => r.json()) # writes carry your token + the {fields} envelope: curl -X POST https://acme.wipp.it/api/data/post \ -H 'Authorization: Bearer wpt_XXXXXX_xxxx' -H 'Content-Type: application/json' \ -d '{"fields":{"title":"From the API","body":"hi"}}'
Data is durable and tenant-owned — it survives wipp uninstalls and template swaps. Object names map directly to the URL slug today.
Brand
Your tenant carries a brand — name, slogan, two logos and primary/secondary colours — applied automatically to every rendered email and report. Set only the fields you want to change:
wippctl brand get wippctl brand set --name "Acme" --slogan "We build things" \ --primary "#326CE5" --secondary "#0EA5E9" \ --logo-for-light-bg https://acme.com/logo-dark.png # used on white: reports, emails \ --logo-for-dark-bg https://acme.com/logo-white.png # used on dark UI: nav, login
Templates read ${brandName}, ${brandSlogan}, ${brandPrimary}, ${brandSecondary}, ${brandLogo} — and every render also gets a ready ${poweredByHtml} “Powered by Wippit” footer element.
Email templates
Override the built-in system emails with your own FreeMarker templates. A template is keyed on name (plus optional iso language and type); subject, html and plain are all FreeMarker source. Your brand is merged into every render, and any <style> CSS is inlined at send time so it survives Gmail & Outlook.
1. Author a template and apply it idempotently (keyed on name+iso+type):
cat > welcome.json <<'JSON'
{
"name": "welcome", "iso": "en",
"subject": "Welcome to ${brandName}, ${name}!",
"html": "<html><head><style>.btn{background:${brandPrimary};color:#fff;padding:10px 18px;border-radius:6px}</style></head><body><h1>Hi ${name}</h1><p><a class=\"btn\" href=\"${url}\">Get started</a></p>${poweredByHtml}</body></html>",
"plain": "Hi ${name}, welcome to ${brandName}. ${url}"
}
JSON
wippctl email-template set -f welcome.json
2. List, inspect, test and delete:
wippctl email-template ls
wippctl email-template get welcome
wippctl email-template test welcome --to you@example.com --model vars.json # renders + sends a sample
wippctl email-template delete welcome
The model exposes your brand and a ${poweredByHtml} footer; --model supplies the template’s own variables as JSON. If you haven’t set a template for a given email, the built-in platform default is used.
Reports
A report template renders tenant data to PDF (or HTML). It declares a set of named datasources — structured queries over your data objects — and a FreeMarker body/header/footer that binds to them by name. No raw SQL: the same injection-safe data plane backs it.
| Datasource type | Yields |
|---|---|
single | one row → ${meta.field} |
list | filtered, sorted rows for a table |
aggregate | scalars — count sum avg min max |
group | per-group counts/totals (grouped summary) |
1. Author the report — datasources + template in one spec:
cat > sales.json <<'JSON'
{
"name": "sales_report", "page_size": "Letter", "output": "pdf",
"datasources": [
{"name":"items", "type":"list", "object":"sales", "order":"region"},
{"name":"byRegion","type":"group", "object":"sales", "groupBy":"region",
"aggregates":[{"as":"orders","fn":"count"},{"as":"total","fn":"sum","column":"amount"}]},
{"name":"totals", "type":"aggregate", "object":"sales",
"aggregates":[{"as":"revenue","fn":"sum","column":"amount"}]}
],
"css": "h1{color:$[BRAND_PRIMARY]} th{background:$[BRAND_PRIMARY];color:#fff}",
"header": "<b>${brandName}</b> — ${brandSlogan}",
"footer": "${poweredByHtml}",
"body": "<h1>Sales</h1>[#list items as s]${s.region}: $${s.amount}<br/>[/#list]<p>Revenue $${totals.revenue}</p>"
}
JSON
wippctl report-template set -f sales.json
2. Generate it — --params bind into the datasource filters at run time:
wippctl report generate sales_report --format pdf --param region=North -o sales.pdf wippctl report-template ls | get <name> | delete <name>
Templates can embed ${qr("text", 120)} and ${barcode("code","CODE_128")} (rendered as images), use $[BRAND_*] placeholders inside CSS, and get your brand + “Powered by Wippit” footer automatically. A list/group filter is {"column","op","param"} — a blank optional param simply drops the filter.
Common-file storage
Storage holds tenant-wide files independent of any template. Two visibilities:
| Visibility | Served at | Access |
|---|---|---|
public | /storage/public/<path> | Open, cached |
private | /storage/private/<path> | Per-path ACL (default: authenticated) |
wippctl storage put public img/logo.svg --from ./logo.svg wippctl storage put private contracts/2026.pdf --from ./2026.pdf wippctl storage list public wippctl storage get private contracts/2026.pdf --to ./local.pdf wippctl storage mv public img/logo.svg img/brand.svg wippctl storage rm public img/old.png
Lock down private paths with selectors — the same grammar as CMS components (authenticated, {"role":"hr"}, {"group":"managers"}):
wippctl storage acl set contracts/ --selector '{"role":"hr"}' # /storage/private/contracts/* → role hr only
wippctl storage acl list
Share privately without accounts. A share link grants passcode-gated read over a private path prefix, and is revocable any time:
wippctl storage share create contracts/ --json # returns a link + passcode
wippctl storage share list
wippctl storage share revoke <share-uid>
Uploaded images are thumbnailed on ingest, so a gallery or media wall reads small derivatives instead of full-size originals.
Custom domains
Happy with acme.wipp.it? Skip this. Otherwise it’s a one-time DNS verification, then HTTPS is automatic.
# 1. Add the alias — prints a TXT record to publish wippctl tenant alias add acme.com --json # _wippit-verify.acme.com. IN TXT "wv_XXXXXXXX" # 2. Publish that exact TXT on your DNS, wait for it to propagate dig +short TXT @1.1.1.1 _wippit-verify.acme.com # 3. Verify ownership wippctl tenant alias verify acme.com wippctl tenant alias list # status flips to "verified" # 4. Point an A record at Wippit (TTL 60) # acme.com. A 32.196.190.96 # www.acme.com. CNAME acme.com.
The first HTTPS request to your domain triggers a Let’s Encrypt cert — only verified aliases pass the issuance gate, so a stranger pointing their domain here gets nothing. A nightly job re-checks the TXT; if you remove it, after repeated failures the alias auto-unverifies. Re-arm with tenant alias regenerate-token acme.com then verify again.
Tokens, users & roles
# API tokens — for CI and AI agents wippctl token list wippctl token issue --name ci-acme --scopes admin # --name required; default scope admin wippctl token revoke <token-uid> # Internal users (employees) — full identity, owned by you wippctl user list # shows UIDs wippctl user create --email alice@acme.com --name "Alice" wippctl user disable <user-uid> wippctl role assign <user-uid> admin wippctl role revoke <user-uid> admin # External users (customers/guests) — handle + roles only, no PII wippctl extuser list wippctl group list # internal org structure (hierarchical) # Branding wippctl tenant settings wippctl tenant settings --update '{"primaryColor":"#0066cc","logoLightUri":"https://acme.com/logo.svg"}'
Privacy by design: internal users live in your tenant with full data you own; external users expose only a handle and their roles — never personal data. Personal data enters only through voluntary wipp form interactions.
Wipps
A “wipp” is a packaged process — forms, cards, scripts, signals, topics — wired together and deployed as a unit. You author it locally and ship it:
wippctl deploy ./my-wipp.wpl wippctl status # what's deployed in this tenant wippctl catalog # available wipps wippctl start <wipp> [--input json-or-key=val] wippctl inbox # cards assigned to you wippctl card <card-uid> # open + submit a card wippctl logs <run-uid> # runtime execution logs wippctl tracking <run-uid> # progress + timeline
Authoring the WPL language itself, exposing forms to your customers, and custom REST/gRPC handlers are deeper topics — ping us at beta@wippit.systems if you want those docs sooner.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
connection error: … EOF | Server URL is grpc:// (plaintext). Use grpcs://wipp.it:443 (TLS). |
code = Unauthenticated | Token expired or wrong env. wippctl env list / wippctl token list. |
expected_etag is required | Another template is active. cms template list --json → pass your etag to activate --expected-etag. |
| SPA assets 404 | Built without the mount as base. Rebuild with --base=/yourmount/. |
| Custom domain returns 404 | A record points here but no template is active for that tenant. Activate one. |
| First HTTPS hit hangs ~5–10s | Caddy is minting the LE cert on demand. Normal on first hit; retry. |
/api/data read returns 401/403 | The object’s read_policy isn’t public (or your token lacks the selector). Re-define with the right policy. |
Alias verify NOT_FOUND | TXT not propagated or has a typo. dig +short TXT @1.1.1.1 _wippit-verify.<host>. |
Anything confusing, missing, or surprising in this flow is a bug — tell us at beta@wippit.systems.
Quick reference
# Setup — installs wippctl + the Claude skill, then just talk to Claude Code curl -fsSL https://get.wipp.it/install.sh | sh # Windows: irm https://get.wipp.it/install.ps1 | iex # Onboarding — start free at account.wipp.it/join, then point wippctl at your tenant wippctl env add <env> --server grpcs://wipp.it:443 --token <wpt_...> ; wippctl env use <env> # Sites wippctl cms template create <name> --display-name "..." --data '{...}' wippctl cms component add <name>/<comp> --uri-mount <path> --type static|spa wippctl cms file write <name>/<comp>/<rel> --from <local> wippctl cms template activate <name> [--expected-etag <e>] | template reload <name> # Data wippctl data define -f spec.json | data ls | data describe <obj> echo '{...}' | wippctl data insert <obj> -f - | data query <obj> --where 'c=v' --sort c --desc wippctl data aggregate <obj> --fn count|sum|avg|min|max [--column c] # Brand, email & reports wippctl brand get | brand set --name "..." --primary "#..." --logo-for-light-bg <url> wippctl email-template set -f tpl.json | ls | get <name> | test <name> --to <addr> | delete <name> wippctl report-template set -f rpt.json | ls | get <name> | delete <name> wippctl report generate <name> --format pdf|html --param k=v -o <file> # Files wippctl storage put public|private <path> --from <file> | storage get|list|mv|rm wippctl storage acl set <prefix> --selector '<sel>' | storage share create <prefix> # Domains wippctl tenant alias add <host> --json → publish TXT → alias verify <host> → A record 32.196.190.96 # Day-2 wippctl token issue --name "..." [--scopes admin,readonly] | token list|revoke wippctl user list|create|disable | role assign|revoke <uid> <role> | group|extuser list wippctl tenant info|settings [--update '{...}'] wippctl deploy <file.wpl> | status | catalog | start | inbox | card | logs | tracking wippctl describe # full command tree as JSON — the authoritative source
If a flag here disagrees with wippctl <cmd> --help, trust --help — wippctl is the source of truth; this page is a snapshot.