Skip to content

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:

  1. Return JSON documents that describe screens (ScreenContract)
  2. Accept saves (changed field values) and persist them
  3. Accept deletes and return the user to a list
  4. Serve lookup lists for relational fields
  5. Validate field values on blur
  6. Serve drill-down data for read-only fields with lookup
  7. Handle custom actions — send email, print, post (optional)
  8. 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:

  1. Fetch a JSON document from a URL
  2. Render that document as a terminal screen
  3. Capture user input
  4. 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:

PriorityRoutePurposePage
1GET /menu/mainMain menuMenus
2GET /screen/{resource}_listList viewLists
3GET /screen/{resource}_card/{id}Card viewCards
4POST /screen/{resource}_card/{id}/saveSaveSaving
5POST /screen/{resource}_card/{id}/deleteDeleteDeleting
6GET /lookup/{field_id}Lookup listLookups
7GET /validate/{field_id}/{value}Blur validationLookups
8GET /drilldown/{field_id}/{key}Drill-downDrill-down
9GET /auth/login + POST /auth/loginAuthenticationAuthentication

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 (except ValidateResponse and AuthResponse).
  • 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 lines array in the save changeset uses the same column ordering as the TableSpec.columns array.

Section index

PageWhat it covers
ScreenContractThe top-level JSON structure every response uses
CardsSingle-record forms with sections and fields
ListsScrollable tables for browsing records
HeaderLinesDocuments with header fields + editable line items
GridsFull-screen editable grids for journals and batch entry
MenusNavigation menus with tabs
Field typesEvery field type and how to define it
LookupsLookup endpoints, blur validation, autofill
Drill-downDrill-down fields and endpoints
SavingHow the client sends changes, how to respond
DeletingDelete flow
Screen ActionsCustom operations: send email, print, post
ValidationLocal rules, blur validation, save validation
AuthenticationLogin flow, JWT, no-auth mode
RoutesAll route patterns and query parameters
LocalizationLocale, work date, UI strings
ProtocolComplete 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

ProblemCause
Client shows blank screenServer not returning valid JSON, or layout field missing
Fields not editableeditable defaults to true, but check if it is explicitly false
Lookup not workingCheck lookup.endpoint URL; the response must have value_column set
Save not workingCheck actions.save URL; the response must be a valid ScreenContract
401 on every requestAuthentication is enabled but no token is sent