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
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.
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
photofield 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.
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 asfromandto. - Four views, and a format per column. Tiles, line, bar and table, with
money,percent,dateandlinkso 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.
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.
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.
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.
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.
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.
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.
The homepage is where the conversation is
FishTea 1.8.12 · beta · the source is private for now.