FishTea

solid ground, liquid UI

Describe a business once.
Derive everything else.

One spec.json gives you the Postgres schema, the classifier's tool list, the planner, the UI, visitor accounts and an operator console. Conversation replaces routing: a small on-device model turns what a visitor says into a shape, the planner composes typed calls, and the answer pours into one of seven vessels.

FishTea 1.8.12, in closed beta. The binary and the source come with the invitation. Start with Welcome to see whether it is for you.

Bun · PostgreSQL + pgvector · needle3 on device · React. No cloud model in the hot path.

Try it: the answer shapes itself

One turn, end to end

What a visitor types, what the classifier understood, what poured, and the provenance underneath it. Every part of this is derived; none of it is a page.

something linen for a summer wedding, under $80

findProducts · fabric: linen · occasion: wedding · max: 80

Three that fit

Ravello linen shirt$68
oatmeal · in stockships Thu

Bari linen trouser$74
sand · two leftships Thu

read products, stock · 41 ms · shape findProducts

not those, something warmer

same shape, next turn · tone: warm

A business is not a set of pages. It is an authority that answers questions.

A shoe store is a place where you can ask anything about shoes. That is the whole definition, and none of it requires a page.

Software is assembled from modules built for everyone, so every variation (a different customer, tier, market or permission) becomes conditional logic draped over something generic. That is inherited, not careless: we store data in rows and columns, so we build software in the shape of that storage, and anything not uniform arrives late as a layer bolted on top. It shows up most painfully in the URL, a string we have spent two decades forcing to act like a memory.

FishTea starts from the other end. Describe what may be asked and by whom, and let the interface be the last and thinnest thing.

  • Browsing is not a funnel. It is how a customer works out what they implicitly like. Most of it is not purchase intent, and treating it as a path to checkout measures the wrong thing.
  • Search is not a result set to filter. The search is the question, and what comes back is an opportunity to ignore or to go deeper.
  • Personalisation stops being a layer. If the interaction is the conversation, every surface is already personal, by what was actually said, not by a profile.

The longer argument →

SPEC

One file

A business described once: entities, shapes, calls, rules, personas. Everything below is derived from it, and nothing may contradict it.

SHAPE

A question it answers

Not a route. A shape is something a visitor may ask, with typed arguments, which is also, exactly, a tool the classifier can choose.

CALL

A typed step

SQL or a compute module, declared with its inputs and outputs. The planner composes calls; the model never writes one.

VESSEL

Where the answer pours

Seven shapes of answer: list, card, detail, form, confirm, timeline, note. Never a page; the vessel is chosen by the answer.

A whole application in one command

Answer an interview about your business; get a running stack. The archetype is a starting point, not a cage; everything it wrote is yours to edit.

$ fishtea new myshop --template ecommerce --interactive
$ cd myshop
$ fishtea up

  db          postgres + pgvector   ready
  classifier  needle3, 3 workers    warm
  core        http://10.0.0.19:3000 listening
  ui          http://10.0.0.19:5173

The visitor types "something linen for a summer wedding, under $80". The classifier picks the findProducts shape and fills its arguments; the planner runs the typed calls the shape names; the answer pours into a list vessel with the provenance strip underneath, saying which shape answered, what it read, what it wrote, how long it took.

Nothing about that is hand-written. It is all derived from the spec.

Accounts

A conversation that remembers who it is talking to

One rule turns on sign-up, sign-in with a password or with Google, a profile the holder owns, and items saved across devices. The account is a row of your own entity, not a parallel idea of a person kept somewhere else.

"rules": {
  "accounts": {
    "entity": "customer",
    "profile": ["name", "photo", "preferences"],
    "saved": { "call": "products.byIds" }
  }
}
  • The account is your row. Signing up creates the customer, or claims the one an email-only form made earlier. Credentials live apart, in the framework's own table: an argon2id password, a Google subject, or both.
  • The profile is a short list, not the table. Only the fields you name can be read or written by the holder. A photo field takes an image; everything else is bounded.
  • A visitor id is still not a credential. Signing in sets the conversation's customer, so the same trail becomes personal, and the account session is its own cookie.

Visitor accounts →

The operator console

Reports and jobs you declare, not build

An application says what its operators need to see and to run; /admin draws it. A report is query calls arranged into panels. A job is an operator shape with a Run button and, if you want, a clock.

"operator": {
  "reports": {
    "performance": {
      "label": "Performance",
      "panels": [
        { "title": "The period",
          "call": "report.totals",
          "view": "tiles",
          "columns": ["visits", "clicks"] },
        { "title": "Clicks",
          "call": "report.daily",
          "view": "line", "x": "day",
          "y": ["visits", "clicks"] }
      ]
    }
  }
}
  • A panel can only read. Its call must be a query; the loader refuses anything else and the core refuses SQL that writes. The period above the page arrives as from and to.
  • Four views, and a format per column. Tiles, line, bar and table, with money, percent, date and link so a number reads the way it means.
  • A job outlives the request. Pressing Run answers immediately and the work carries on in the core, because a catalogue sync takes longer than a proxy will wait. Every run is kept, so the last good answer survives a restart.
  • Or it runs itself. "schedule": { "daily": ["05:00"], "afterDeploy": true }: a missed run catches up when the core comes back, and a new release fills what it needs without a plain restart doing it again.

The console →

Echoes

What the visitor did next

An answer can end somewhere else: the shop that sells it, the paper behind the citation, the map to the venue. Until echoes, the conversation lost the visitor at that point. It knew what it had shown and not what they followed.

Ravello linen shirt$68
oatmeal · in stockat Ravello →

read products · 41 ms · echo ravello.example → turn 4

  • The trail is the signature. A destination is followed only when the conversation published it: the turn's stored pour must contain that URL exactly. A link no answer ever carried is refused.
  • So it is not an open redirect, and it needs no signing secret: there is no token to mint, rotate or leak.
  • It is the fact a business needs. An affiliate site joins a sale back to the question that produced it; a publication sees which sources readers trust.

Echoes →

Built with it

Two of these are live right now

Not demos. Two businesses in production on this framework, each one leaning on a different part of it, each with a write-up of what the spec actually bought.

ChicDuJour

A fashion shop that owns no stock. Moods are declared once in rules.moods and read by four things that never meet: the classifier's hints, the catalogue search, the copy at the top of the answer, and the overnight vendor sync. Suppliers are rows with adapters; the catalogue is a cache with an expiry. A stranger who says "save this" gets the form that unblocks it, not a wall.

Read the case study → · chicdujour.com

shawnstewart.me

A portfolio built around the one question recruiters actually arrive with. Paste a job description and a compute call returns it requirement by requirement, each marked strong, partial or gap with the evidence rows behind it. No model grades anything. The write-up includes the labelled set's first score, which was 37%, and what that turned out to mean.

Read the case study → · shawnstewart.me

Start from an archetype

Five, each one already running

Every archetype is a complete application with seed data and an onboarding interview that turns it into your business.

ecommerce
A catalogue, a cart, orders, and vendor feeds, so stock can come from somewhere else.
portfolio
A person you can interview: skills, projects, an honest assessment of fit.
verification
Consent, attestation, and disclosure limited to the purpose asked for.
program
Sessions and registration: capacity, ages, sibling discounts, scholarships.
blank
One entity, one read, one write. Start here when nothing else fits.

Plugins for everything outside the conversation

Six seams, one mechanism. A plugin is a directory with a manifest; what it contributes is merged when the spec loads and never written into your spec.json.

integration

Calls, entities and shapes: Stripe, a frontier model, a shipping quote.

ui

A design system: tokens, and optionally its own vessels.

auth

How a visitor becomes known.

classifier

An alternative to the on-device needle pool.

channel

Where a notification is delivered.

archetype

A template fishtea new can start from.

How plugins work →

Upgrade without regenerating

An application pins a framework range and records where every file came from, so an upstream fix reaches it without touching a line you wrote.

$ fishtea update

framework  1.5.0 → 1.6.0  (pinned ^1.5.0)
spec       format 1.1 → 1.2
files      2 to update, 1 new, 1 conflicting, 3 yours

   ↑ calls/cart_add.sql
   + calls/products_newest.sql
   ! calls/products_related.sql — you changed it and so
     did the archetype; the new one lands as .new

Every framework version you install stays on disk and fishtea use <version> switches between them by moving one symlink. Going back is as cheap as going forward, which is the only reason going forward is comfortable.

Versioning and updates →

The homepage is where the conversation is

FishTea 1.8.12 · beta · the source is private for now.