Wippit Start free →

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.

No workspace yet? Start free — first month free, then $19/mo — 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? Start free.

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 sign-up to live

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.

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

Ready when you are.

No workspace yet? Start free and you’re publishing in minutes.

Start free →