dBASE III is back. In your browser. USE customers like it's 1984.
Remember the dot prompt? Before SQL won, before ORMs, before anyone said "full-stack" — there was dBASE III. You typed USE customers, then LIST, and your data was just there. WebBase-III brings that whole world back: the terminal, the language, BROWSE, @ SAY GET forms, .prg programs, indexes, reports — rebuilt from scratch as a modern web app with its own interpreter in TypeScript, backed by Node.js, WebSockets, and SQLite.
Try it in one click — no install:
The Codespace installs dependencies and starts the dev server automatically. Open the forwarded port 5173 and you're at the dot prompt.
The command interface — type W3Script and see results instantly.
LIST prints all records in active index order. The status bar shows the active database and table.
INDEX ON name TO BYNAME creates a SQLite index and activates it — subsequent LIST output is sorted alphabetically. SEEK "Delta NV" jumps the record pointer to the first match in O(log n).
BROWSE opens a spreadsheet-style grid. Records are shown in active index order. Tab/Enter to edit a cell, Ctrl+N for a new row, Delete to remove a row, Esc to return to the terminal.
EDIT <name> opens the built-in .prg source editor. Programs support the full W3Script language: DO CASE/ENDCASE, DO WHILE/ENDDO, IF/ENDIF, form layouts, and all data commands. Ctrl+S saves, Esc cancels.
@ row,col SAY "label" GET variable lays out character-cell form fields. READ renders them as a live form and waits for the user to fill in values and submit.
The permanent left sidebar with category pickers and action buttons.
Wizards open in the main area with a live W3Script preview.
MODIFY STRUCTURE (or sidebar action) opens the column editor prefilled with the current schema — rename, retype, add, or drop columns, then apply in one click.
Grid edits are checked against the column's declared type before they commit. An invalid
value keeps the cell in edit mode, outlined in red, and says why; the error clears as soon
as you fix it, and Esc abandons the edit. Here a TIME(15) column rejects 08:07.
SUM, AVERAGE, ? ROUND(…), ? MAX(…), and SORT ON … TO — numeric aggregates and sorted copies, honouring the active filter.
COPY TO <file>.csv exports the current table (browser download); APPEND FROM <file>.csv imports via the file picker.
| Feature | Details |
|---|---|
| W3Script interpreter | dBASE III command dialect: navigation, filters, variables, loops, conditionals, forms, programs |
| BROWSE grid | Inline cell editing, keyboard nav, index-ordered display |
| Form engine | @ ROW,COL SAY … GET character-cell layout with READ |
| Indexing | INDEX ON, SEEK, FIND — active index controls all record order |
| DO CASE | Multi-branch conditional, OTHERWISE fallback |
| Built-in functions | EOF(), BOF(), FOUND(), RECNO(), SUBSTR(), STR(), AT(), CTOD(), DTOC() and more |
| Program files | Save, edit, and run .prg scripts with DO / EDIT |
| The Assistant | Permanent left sidebar — open databases/tables, browse, filter, CSV import/export, sort, sum/average, index, search, reindex, pack, design reports, run programs without typing |
| Multi-user | Each WebSocket connection gets its own isolated interpreter session |
| Live propagation | Edit a record in one session and every other session BROWSE-ing that table refreshes automatically — no re-query |
| Persistent storage | better-sqlite3 with WAL mode — databases survive server restart |
The sidebar on the left drives everything without typing: open or create databases and tables, browse and filter data, import/export CSV, sort into a new table, sum or average a numeric field, build indexes, search, reindex, pack the database, design and run reports, run programs, and modify table structure. Every click generates a real W3Script command that echoes into the terminal — watch it to learn the language. Wizards (New table, Filter, Sort, Sum/Average, Modify structure, report designer, …) open in the main area and show a live preview of the command they will run.
npm install
npm run dev # http://localhost:5173Production:
npm run serve # builds, then serves everything on http://localhost:3000LAN / Tailscale: the server binds to 0.0.0.0, so http://<tailscale-ip>:3000 works out of the box.
USE DATABASE mydb
CREATE TABLE customers (name CHAR(40), phone CHAR(20), country CHAR(30))
USE customers
APPEND RECORD
REPLACE name WITH "Acme Corp", phone WITH "555-1234", country WITH "BE"
APPEND RECORD
REPLACE name WITH "Zeta Ltd", phone WITH "555-5678", country WITH "NL"
INDEX ON name TO BYNAME
LIST * sorted A→Z
SEEK "Zeta Ltd" * jump to record instantly
BROWSE * open editable grid
SET FILTER TO country == "BE"
LIST * filtered view
SET FILTER TO * clear filter
WebBase-III supports unlimited work areas — each independently holding a table, record pointer, filter, and index. Link areas by key field using SET RELATION TO for relational data access. Cross-area field access uses alias.field dot notation.
Note: dBASE III supported a maximum of 10 work areas (DOS file handle limit). WebBase-III has no such limit. dBASE III used
alias->fieldarrow syntax; WebBase-III uses modernalias.fielddot notation.
| Command | What it does |
|---|---|
SELECT <alias> |
Activate (or create) a work area by name |
USE <table> [ALIAS <name>] |
Open table in active area; optional alias override |
SET RELATION TO <expr> INTO <alias> |
Link active area to another; auto-seeks on every navigation |
SET RELATION TO |
Clear relation on active area |
LIST [col, alias.col, ...] |
List records; optional column list with cross-area fields |
LIST AREAS |
Show all open work areas, pointers, indexes, and relations |
CLOSE |
Close active area's table |
CLOSE ALL |
Close all work areas, reset to single empty area 1 |
Cross-area field access: use alias.field dot notation anywhere an expression is accepted — SET FILTER TO, IF, REPLACE, LIST, INDEX ON.
| Command | What it does |
|---|---|
USE <table> |
Select a table; restores any saved active index |
USE DATABASE <name> |
Open a named SQLite database |
LIST |
Print records in active index order (up to 500) |
LIST STRUCTURE |
Show column schema |
LIST TABLES |
Show all tables with record counts |
LIST DATABASES |
Show all databases on disk (alias: LIST DBS) |
BROWSE |
Open the editable grid |
CLEAR |
Clear terminal output |
CREATE TABLE <n> (col TYPE, ...) |
Create a table |
DROP TABLE <name> |
Delete a table |
APPEND RECORD |
Insert a blank row |
DELETE / DELETE ALL |
Delete current or all records |
PACK |
VACUUM the SQLite file |
GO TOP / GO BOTTOM / GO <n> |
Move record pointer |
SKIP <n> |
Move pointer forward/back |
REPLACE <field> WITH <val>, ... |
Update field(s) on current row |
REPLACE ALL <field> WITH <val>, ... |
Update all (filtered) rows |
SET FILTER TO <expr> |
Set a WHERE clause; empty clears it |
SUM <field> [FOR <cond>] [TO <var>] |
Total a numeric field over the current table (honours active filter); TO <var> stores the result in a variable instead of printing |
AVERAGE <field> [FOR <cond>] [TO <var>] |
Mean of a numeric field over the current table (honours active filter); TO <var> stores the result in a variable instead of printing |
COPY TO <file>.csv |
Export current table to a CSV download (honours filter + index order) |
APPEND FROM <file>.csv |
Import a CSV (browser file picker) into the current table |
MODIFY STRUCTURE |
Open the Modify-structure wizard for the active table |
ALTER TABLE <t> ADD <col> <type> |
Add a column to a table |
ALTER TABLE <t> DROP <col> |
Remove a column from a table |
ALTER TABLE <t> RENAME <col> TO <new> |
Rename a column |
ALTER TABLE <t> ALTER <col> <type> |
Change a column's type (copy-table dance; data preserved) |
Column ops that can invalidate an index (DROP, RENAME, ALTER type) drop all of the table's indexes and warn you to rebuild with
INDEX ON.
CREATE TABLErejects a malformed column list (missing comma, missing type, unclosed paren, a third type argument) with a parse error naming the offending column, and creates nothing.
Column types: CHAR(n) (aliases CHARACTER/VARCHAR/STRING/MEMO), NUM/NUM(p,s) (NUMERIC/FLOAT/DOUBLE/DECIMAL), INT/INTEGER, LOGICAL/BOOLEAN, DATE, and TIME/TIME(n). TIME stores HH:MM (24-hour); the optional TIME(n) qualifier (e.g. TIME(15)) requires minutes to be a multiple of n. REPLACE ... WITH rejects a malformed or off-granularity TIME value instead of silently coercing it, and LIST STRUCTURE prints the declared type (NUM(8,2), TIME(15)) rather than SQLite's storage class.
CSV format (
COPY TO/APPEND FROM): Unlike dBASE III's headerless, positionalDELIMITED/SDFformats, WebBase-III uses modern header-based CSV (RFC-4180, mapped by column name). Export downloads through the browser and honours the active filter + index order. Import opens a file picker and is lenient: up to 10 bad rows are skipped and reported (line + reason); more than 10 aborts with nothing appended. Limits: 5 MB import, 50,000-row export.
| Command | What it does |
|---|---|
INDEX ON <expr> TO <tag> |
Create index on expression; sets it active immediately |
SET INDEX TO <tag> |
Activate a previously created index |
SET INDEX TO |
Clear active index — restores natural insert order |
REINDEX |
Rebuild SQLite indexes for current table |
LIST INDEXES |
Print all indexes for current table with * active marker |
SEEK <expr> |
Position record pointer at first index match |
FIND <string> |
Alias for SEEK (unquoted string — dBASE III legacy form) |
SORT ON <field>[/D] TO <newtable> |
Write a sorted copy of the table to a new table; /D = descending; honours the active filter |
JOIN WITH <alias> TO <file> FOR <cond> [FIELDS <list>] |
Materialize a snapshot table by joining the active area with <alias>; honours the active filter |
JOINdiffers from classic dBASE III:FORis required (dBASE allowed omitting it); cross-area fields usealias.fielddot syntax (notalias->field);FORis a SQL predicate (likeSET FILTER); the join runs on SQLite's planner, not an O(n×m) nested loop; on a column-name clash the active table wins and the duplicate is dropped with a warning (dBASE dropped it silently); and there are no 128-field / 10-char-name limits.Limitation: if a
SET FILTERis active on the source area, its predicate is applied to the joined query; a filter referencing a column whose name exists in both joined tables may be ambiguous — qualify it or clear the filter before JOIN.
| Command | What it does |
|---|---|
CREATE REPORT <name> |
Create a new report definition (opens JSON editor) |
MODIFY REPORT <name> |
Edit an existing report definition |
REPORT FORM <name> |
Run report — ASCII to terminal + HTML preview panel |
LIST REPORTS |
List all saved report definitions |
DELETE REPORT <name> |
Delete a report definition |
| Command | What it does |
|---|---|
DO <name> |
Run a saved .prg program |
EDIT <name> |
Open .prg source editor |
LIST PROGRAMS |
Show all saved programs |
Demo programs live in
demos/*.prgand are the single source of truth: they are seeded into the program store on every server start, overwriting any store copy. Matching report definitions live indemos/reports/*.jsonand are seeded the same way.Three of them are usable example apps you can build off — run them, then
EDITto adapt:
DO crm— a mini-CRM: companies, contacts, and deals with pipeline totals (SUM … FOR), top-deals sort, a grouped report, CSV export, and a companies+deals JOIN.DO inventory— a stock manager: categories, products (with reorder levels), and a stock-movements ledger, plus valuation totals, a low-stock report, sort, CSV export, and a products+categories JOIN.DO overtime— an overtime tracker: per-employee weekly schedules,TIME(15)timesheets you edit in the validated grid,WEEK()/DATEADD()week handling, a live overtime balance (banked minus leave taken), a grouped report, and CSV export.They lean on multi-work-area relations (
alias.field), indexes, and@ SAY … GET/READforms, and invite you to open a second window to watch live multiuser propagation.
| Command | What it does |
|---|---|
? <expr>[, <expr>...] |
Evaluate expression(s) and print the result (numbers right-justified; bare ? prints a blank line; ?? also accepted) |
STORE <val> TO <var> |
Assign a variable |
INPUT "prompt" TO <var> |
Collect keyboard input |
@ r,c SAY "text" GET <var> |
Define a form field |
READ |
Display the form and wait for submit |
| Command | What it does |
|---|---|
IF <cond> … ENDIF |
Conditional block |
DO WHILE <cond> … ENDDO |
Loop |
DO CASE … ENDCASE |
Multi-branch conditional (CASE, OTHERWISE) |
HELP |
Print command reference |
QUIT |
Exit |
Functions work anywhere an expression is accepted — IF, DO WHILE, STORE, REPLACE, INDEX ON, SET FILTER TO, etc.
| Function | Returns |
|---|---|
EOF() |
True if record pointer is past last record |
BOF() |
True if record pointer is before first record |
FOUND() |
True if last SEEK / FIND matched |
RECNO() |
Current record number |
RECCOUNT() |
Total records in current table |
UPPER(str) |
Uppercase |
LOWER(str) |
Lowercase |
TRIM(str) |
Strip leading and trailing spaces |
LTRIM(str) |
Strip leading spaces only |
SUBSTR(str, start, len) |
Substring — 1-based; len optional (to end) |
LEN(str) |
String length |
AT(needle, haystack) |
1-based position; 0 if not found (case-sensitive) |
STR(num, len, dec) |
Number to right-justified string; default len=10, dec=0 |
VAL(str) |
String to number; non-numeric → 0 |
INT(n) |
Truncate toward zero |
ABS(n) |
Absolute value |
SPACE(n) |
String of n spaces |
REPLICATE(str, n) |
Repeat string n times |
DATE() |
Today as MM/DD/YY |
DTOC(date) |
Date to display string MM/DD/YY |
CTOD(str) |
Display string MM/DD/YY to ISO date |
ROUND(num, dec) |
Rounds number to dec decimal places (default 0) |
MOD(a, b) |
Remainder of a divided by b |
MAX(a, b) |
Maximum of a and b |
MIN(a, b) |
Minimum of a and b |
TIME() |
Current time as HH:MM:SS |
YEAR(date) |
Numeric year from ISO date string |
MONTH(date) |
Numeric month from ISO date string |
DAY(date) |
Numeric day from ISO date string |
WEEK(date) |
ISO-8601 week number (1–53) — Monday-start weeks, week 1 holds the year's first Thursday |
DATEADD(date, n) |
ISO date n days later (n may be negative); '' for an invalid date |
W3Script supports both styles:
| Syntax | Value |
|---|---|
TRUE / FALSE |
Boolean true/false |
.T. / .TRUE. |
Boolean true (dBASE III style) |
.F. / .FALSE. |
Boolean false (dBASE III style) |
Boolean values display as .T. / .F. in output to match dBASE conventions.
Logical operators are accepted in both styles too: NOT / .NOT., AND / .AND., OR / .OR. (e.g. DO WHILE .NOT. EOF()).
| Key | Action |
|---|---|
| Arrow keys | Navigate cells |
| Enter / F2 | Edit selected cell |
| Tab / Shift+Tab | Move right / left |
| Ctrl+N | New row |
| Delete | Delete current row |
| F5 | Refresh from DB |
| Esc | Exit grid, return to terminal |
Cell validation. An edit is checked against the column's declared type before it commits. An invalid value keeps the cell in edit mode, outlined in red, with the reason shown (
HH:MM,multiple of 15,at most 2 decimal place(s),not a real date); the error clears as soon as you fix it, andEscabandons the edit.DATE,TIME/TIME(n),NUM(p,s),INTandLOGICALare validated;CHAR/MEMOare not. The server re-checks every edit independently.
server/
index.ts Node.js HTTP + WebSocket server (port 3000)
Session.ts Per-connection session: parses commands, drives Executor
SessionManager.ts Tracks all active sessions
ServerDatabaseBridge.ts IDatabaseBridge impl wrapping better-sqlite3
ProgramStore.ts .prg program storage in data/system.sqlite3
IndexStore.ts Index metadata + active index in data/system.sqlite3
src/
interpreter/
Lexer.ts Tokenises W3Script input (case-insensitive)
Parser.ts Recursive-descent AST builder
Executor.ts Async AST runner; manages db/table/filter/vars/rowPtr/activeIndex
Builtins.ts Stateless built-in function implementations
terminal/
Terminal.ts REPL UI — command history, multi-line block accumulation
ui/
Grid.ts BROWSE spreadsheet — inline cell editing, keyboard nav
FormLayout.ts @ SAY GET form engine — character-cell coordinates
ProgramEditor.ts .prg source editor UI
ws/
WsClient.ts Browser WebSocket client
npm test # unit + integration tests (Vitest)
npx playwright test # end-to-end browser tests (auto-starts the dev server)The Playwright suite drives a real browser against the running app and covers the
REPL, filters, indexing, programs, forms, BROWSE, the Assistant wizards,
multi-work-area, the parity commands (?/??, SUM/AVERAGE, SORT ON … TO),
and CSV COPY TO/APPEND FROM. CI runs both Vitest and Playwright on every PR.
Contributions are welcome! WebBase-III uses GitFlow — fork the repo and open your
PR against the active release/vX.Y.Z branch (the open milestone), not main.
See CONTRIBUTING.md for the full fork → release-branch workflow and
the Definition of Done.
Huge thanks to everyone who has helped extend WebBase-III:
- @kas2804 (Kasturi Rajarampatil) — added the
ROUND,MOD,MAX,MIN,TIME,YEAR,MONTH, andDAYbuilt-in functions (#4, PR #17) — clean, well-tested work that brought the W3Script expression engine closer to full dBASE III parity. 🙌
AGPL-3.0 — see LICENSE.md.
Why AGPL? WebBase-III is a toy, and the license keeps it that way: anyone can use it, fork it, and learn from it, but nobody can take it closed and sell it as a hosted service without giving their changes back. If you want to run it, hack it, or ship features from your dBASE memories — that's exactly what it's for.












