Server Implementation
Summary
Everything you need to build a 2Wee-compatible server in any language or framework. This is the primary guide for server implementors.
What you are building
The 2Wee client is a terminal browser. Your server is a website for that browser. Instead of returning HTML, you return JSON. Instead of CSS, you use field types and styles. Instead of JavaScript, the client handles all interaction based on the protocol.
Your server's job:
- Return JSON documents that describe screens (ScreenContract)
- Accept saves (changed field values) and persist them
- Accept deletes and return the user to a list
- Serve lookup lists for relational fields
- Validate field values on blur
- Serve drill-down data for read-only fields with lookup
- Handle custom actions — send email, print, post (optional)
- Authenticate users (optional)
The dumb client
The 2Wee client contains zero business logic. It does not know what a customer is, what an invoice looks like, or how to calculate a balance. It knows how to:
- Fetch a JSON document from a URL
- Render that document as a terminal screen
- Capture user input
- Send changes back to the server
This is the single most important design principle. Everything else follows from it. The server describes what to show. The client decides how to render it. You never think about terminal rendering, cursor movement, or keyboard handling.
The mental model
Your Server 2Wee Client
│ │
│── ScreenContract (JSON) ──────────>│
│ "Here is a card with these │── renders fields, tables,
│ fields, these sections, │ sections in the terminal
│ these action URLs" │
│ │
│<── SaveChangeset (JSON) ───────────│
│ "The user changed these │── user pressed Ctrl+S
│ 3 fields" │
│ │
│── ScreenContract (JSON) ──────────>│
│ "Saved. Here is the │── replaces current screen
│ updated card" │Form-level save
The user edits fields freely. No data is sent to the server until the user presses Ctrl+S. The client collects all changed fields into a changeset and sends it as a single POST. The server responds with a fresh ScreenContract reflecting the saved state.
No per-field round-trips during editing. The one exception is lookup blur validation — validated lookup fields trigger an async GET on field exit to check if the value exists in the related table. This is a read, not a write.
What you need to implement
Start with these routes and add more as needed:
| Priority | Route | Purpose | Page |
|---|---|---|---|
| 1 | GET /menu/main | Main menu | Menus |
| 2 | GET /screen/{resource}_list | List view | Lists |
| 3 | GET /screen/{resource}_card/{id} | Card view | Cards |
| 4 | POST /screen/{resource}_card/{id}/save | Save | Saving |
| 5 | POST /screen/{resource}_card/{id}/delete | Delete | Deleting |
| 6 | GET /lookup/{field_id} | Lookup list | Lookups |
| 7 | GET /validate/{field_id}/{value} | Blur validation | Lookups |
| 8 | GET /drilldown/{field_id}/{key} | Drill-down | Drill-down |
| 9 | GET /auth/login + POST /auth/login | Authentication | Authentication |
See Routes for the complete route reference including query parameters and URL architecture.
Transport rules
- GET for reads, POST for writes. No PUT, PATCH, or DELETE.
- Every response is JSON with
Content-Type: application/json. - Screen responses are always a
ScreenContract(exceptValidateResponseandAuthResponse). - All field values are strings. Your server formats everything — numbers, dates, currencies.
- The client does not construct URLs. Your server provides all URLs in
actions,on_select,lookup.endpoint, etc.
Additional principles:
- The changeset is sparse. The client sends only changed fields, not the entire form.
- Column order is the contract for grid lines. The
linesarray in the save changeset uses the same column ordering as theTableSpec.columnsarray.
Section index
| Page | What it covers |
|---|---|
| ScreenContract | The top-level JSON structure every response uses |
| Cards | Single-record forms with sections and fields |
| Lists | Scrollable tables for browsing records |
| HeaderLines | Documents with header fields + editable line items |
| Grids | Full-screen editable grids for journals and batch entry |
| Menus | Navigation menus with tabs |
| Field types | Every field type and how to define it |
| Lookups | Lookup endpoints, blur validation, autofill |
| Drill-down | Drill-down fields and endpoints |
| Saving | How the client sends changes, how to respond |
| Deleting | Delete flow |
| Screen Actions | Custom operations: send email, print, post |
| Validation | Local rules, blur validation, save validation |
| Authentication | Login flow, JWT, no-auth mode |
| Routes | All route patterns and query parameters |
| Localization | Locale, work date, UI strings |
| Protocol | Complete JSON wire format specification |
Worked examples
- Sales Order — HeaderLines, item lookups with autofill, polymorphic lookups, calculated columns, and saving.
- Banking Journal — Grid layout, account lookups, totals footer, and posting.
Getting started
Verification
Test your server with curl:
sh
# Fetch menu
curl -s http://localhost:3000/menu/main | jq .
# Fetch a list
curl -s http://localhost:3000/screen/customer_list | jq .
# Fetch a card
curl -s http://localhost:3000/screen/customer_card/10000 | jq .Then connect the 2Wee client and verify rendering and interaction.
Troubleshooting
| Problem | Cause |
|---|---|
| Client shows blank screen | Server not returning valid JSON, or layout field missing |
| Fields not editable | editable defaults to true, but check if it is explicitly false |
| Lookup not working | Check lookup.endpoint URL; the response must have value_column set |
| Save not working | Check actions.save URL; the response must be a valid ScreenContract |
| 401 on every request | Authentication is enabled but no token is sent |