Wippit Beta access →

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)
Orientation

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/data REST 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.

Invite-only beta. No code yet? Request access, or email hello@wippit.systems.

The part that feels like a trick

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? Request a beta invite.

The magic, three ways

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.

10 minutes from invite to live

Quickstart

1. Redeem your invite code. You don’t have a token yet, so this one step is a plain REST call (see /redeem):

curl -s -X POST https://wipp.it/redeem -H 'Content-Type: application/json' -d '{
  "code":         "sc_XXXXXXXXXXXXX",
  "customerName": "acme",
  "ownerName":    "Acme, Inc.",
  "ownerEmail":   "you@acme.com"
}' | jq

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. customerName is lowercase letters/digits, 3–63 chars, and permanent — it’s your subdomain.

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 in your invite? 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.

The model in five words

Core concepts

ThingWhat it is
TemplateYour site. One is active at a time; activation is atomic. Carries a data map substituted into files.
ComponentA 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 objectA tenant-owned table you declare with a JSON spec, read/written over /api/data with its own access policy. §Data
StorageTenant-wide files separate from any template — public (open) or private (per-path ACL + share links). §Files
WippA packaged process automation — forms, cards, signals — deployed as a unit. §Wipps
Token / roleAPI 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.

Static pages

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.

Single-page apps

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.

Store & serve structured data

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

VerbPathDoes
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.

Your identity, applied everywhere

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.

Branded transactional email

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.

Data → PDF

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 typeYields
singleone row → ${meta.field}
listfiltered, sorted rows for a table
aggregatescalars — count sum avg min max
groupper-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.

Files, images & share links

Common-file storage

Storage holds tenant-wide files independent of any template. Two visibilities:

VisibilityServed atAccess
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.

Bring your own domain

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.

Operate it

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.

Process automation

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.

When something’s off

Troubleshooting

SymptomCause & fix
connection error: … EOFServer URL is grpc:// (plaintext). Use grpcs://wipp.it:443 (TLS).
code = UnauthenticatedToken expired or wrong env. wippctl env list / wippctl token list.
expected_etag is requiredAnother template is active. cms template list --json → pass your etag to activate --expected-etag.
SPA assets 404Built without the mount as base. Rebuild with --base=/yourmount/.
Custom domain returns 404A record points here but no template is active for that tenant. Activate one.
First HTTPS hit hangs ~5–10sCaddy is minting the LE cert on demand. Normal on first hit; retry.
/api/data read returns 401/403The object’s read_policy isn’t public (or your token lacks the selector). Re-define with the right policy.
Alias verify NOT_FOUNDTXT 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.

One page to keep open

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
curl -X POST https://wipp.it/redeem -H 'Content-Type: application/json' -d '{...}'
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.

Ready when you are.

No workspace yet? Grab a beta invite and you’re publishing in minutes.

Request beta access →