What is FoxPro Ninja?

FoxPro Ninja (powered by Ninja) is a self-hosted server that exposes your existing Visual FoxPro and DBF data over a standard REST HTTP API. It sits alongside your running FoxPro installation — reading and writing the same files — without requiring any schema migration, database replacement, or downtime.

Once running, any web application, reporting tool, mobile app, or integration script can query and update your FoxPro data using ordinary HTTP requests and JSON. No FoxPro SDK, no ODBC driver, no special client library required.

FoxPro Ninja is built around three layers:

  • Layer 1 — Table API. Every DBF table in your data directory is immediately available as a REST endpoint. No configuration needed beyond pointing the server at your data folder.
  • Layer 2 — Aggregate Engine. Named, YAML-defined operations that combine multiple tables into a single transactional unit, enforcing the same business rules your FoxPro screens relied on.
  • Layer 3 — Admin UI. A browser-based interface for exploring tables, browsing data, and managing aggregate definitions.

Architecture

FoxPro Ninja is a single binary with no external runtime dependencies. It runs on the same Windows server as your FoxPro application, directly alongside it.

Incoming HTTP requests are authenticated, routed to the appropriate handler (table CRUD or aggregate engine), and executed against your DBF files by the NinjaCore. Every write is journaled to an append-only audit log before the response is returned to the caller.

System overview
  Browser / Reporting Tool / Integration
              │
              │  HTTPS (Bearer token)
              ▼
  ┌─────────────────────────────────┐
  │         Ninja server             │
  │                                 │
  │  Auth → Layer 1 (Table CRUD)   │
  │       → Layer 2 (Aggregates)   │
  │       → Layer 3 (Admin UI)     │
  │                                 │
  │  ┌──────────────────────────┐  │
  │  │    NinjaCore         │  │
  │  │  (read / write / index)  │  │
  │  └──────────┬───────────────┘  │
  └─────────────┼───────────────────┘
                │
  ┌─────────────▼───────────────────┐
  │   Your data directory           │
  │   *.DBF  *.FPT  *.CDX  *.DBC   │
  └─────────────────────────────────┘
              ▲
              │  (FoxPro still runs here too)
  Your FoxPro Application

FoxPro and Ninja coordinate through the same file-level locking mechanisms FoxPro has always used — so both can be active simultaneously without conflict.

NinjaCore

At the core of FoxPro Ninja is NinjaCore — a proprietary, high-performance engine developed by F8 Labs for reading and writing DBF, FPT (memo), and CDX (compound index) files with full Visual FoxPro compatibility.

The engine provides:

  • Full VFP field type support — Character, Numeric, Float, Integer (including autoincrement), Date, DateTime, Logical, Memo (text FPT), Currency, Double, Blob, and variable-length Varchar/Varbinary. Every surfaced type round-trips correctly through the JSON API. General/OLE (G) and Picture (P) columns carry raw binary blobs that cannot be expressed as JSON; they are intentionally omitted from schema and row responses.
  • Nullable column support — columns marked as nullable in the VFP Database Designer return JSON null when the VFP null flag is set, rather than an empty string, zero, or sentinel value. The GET /tables/{name} schema endpoint reports which columns are nullable.
  • CDX index maintenance on every write — structural, compound, and tag-filtered indexes are kept current so FoxPro SEEK operations continue to work correctly after API writes.
  • FPT file support — both text memo fields and binary General/OLE fields read from and write to the companion .fpt file.
  • Live header refresh on every read — rows appended by FoxPro between API requests are visible immediately, without a restart or file close/reopen.
  • Row-level locking using VFP's standard byte-range locking protocol, enabling safe concurrent access alongside a live FoxPro session.
  • DBC metadata parsing — reads the Database Container to discover long table names, field defaults, validation rules, and referential integrity relationships defined in the VFP database designer.
ℹ️

NinjaCore is proprietary to F8 Labs and ships as part of the Ninja binary. It is not separately available.

FoxPro Coexistence

FoxPro Ninja is designed from the ground up to run alongside a live FoxPro application on the same data files. You do not need to stop FoxPro, take a backup, or schedule a maintenance window to run the API.

How locking works

When Ninja writes a row, it acquires the same byte-range record lock that FoxPro's RLOCK() uses. If FoxPro already holds the lock, the API request waits (up to the configured timeout) and retries. FoxPro behaves identically when the API holds a lock — neither side blocks the other's reads, only concurrent writes to the same row.

Read visibility

NinjaCore re-reads the DBF header on every scan and get operation. Rows that FoxPro appends between API requests are visible immediately — no restart or cache invalidation required.

Starting in read-only mode

We recommend starting with read_only: true in your config while you validate that the API is returning correct data. In read-only mode all POST, PATCH, and DELETE requests are rejected at the HTTP layer — FoxPro remains the only writer. Flip the flag when you're confident.

⚠️

Do not run FoxPro's PACK command while Ninja is active. PACK physically removes deleted rows and renumbers every record. Any pagination cursor issued before the PACK will reference a recno that no longer points to the same row. Schedule PACK during a maintenance window when Ninja is stopped. FoxPro itself enforces this — PACK requires an exclusive table lock, which fails while Ninja holds the file open.

ℹ️

Ninja detects PACK automatically. If a PACK occurs between a Ninja stop and restart, the engine notices the row-count change on the first read and logs an error. Check GET /health/details — the pack_events field lists any detected occurrences with the table name, timestamp, and before/after record counts.

DBC Support

If your FoxPro application uses a Database Container (.dbc) — which Visual FoxPro shops typically do — Ninja reads it at startup and uses its metadata throughout the engine's lifetime.

From the DBC, Ninja extracts and enforces:

  • Long table names — the DBC display name is used in API responses and accepted in URL paths alongside the 8.3 short name.
  • Field defaultsDefaultValue expressions are evaluated when a field is omitted from an insert body.
  • Validation rulesRuleExpression constraints are checked on writes using a bounded expression evaluator.
  • Referential integrityRIINFO cascade relationships (restrict, cascade, set null) are enforced in the engine on deletes and key-updates, including multi-level cascades with cycle detection.

Point data_dir at the folder containing your .dbc file and the engine discovers everything automatically. No extra configuration is needed to enable DBC support.

Validating Your Data

Before going live, confirm that Ninja can open every table in your data directory and read every field type without error. This is especially important if your schema uses less-common VFP types — nullable columns, autoincrement integers, Currency, or General/OLE binary fields.

Pre-flight: start Ninja and check /health/details

Start Ninja pointed at your data directory (in read-only mode if preferred) and call the health endpoint with an admin token:

request
GET /health/details
Authorization: Bearer <admin-token>

The response tells you everything Ninja found at startup:

  • tables — list of every DBF file that was opened, with its row count and column count. A table missing here means Ninja could not open it (bad path, corrupt header, or unsupported version).
  • stale_on_write_tags — CDX index tags that Ninja cannot maintain on writes because their key expressions use FoxPro functions that require a live VFP evaluator. Ninja logs these at startup so you know to let FoxPro rebuild them after a write session. A clean installation shows an empty array.
  • pack_events — any PACK or ZAP operations detected since the last start (table name, timestamp, before/after row count). Should be empty; a non-empty list means a PACK ran while Ninja was stopped and pagination cursors issued before that PACK are now stale.

A clean pre-flight shows every expected table in tables, empty stale_on_write_tags and pack_events, and the status ok at the top level. If a table is missing, check the Ninja log for the open error — it names the exact file.

Spot-checking field types

Once the server is running, call GET /tables/{name} on any table to see its schema, including which columns are nullable and what VFP type each column uses. Then call GET /tables/{name}/rows and compare a few rows against what FoxPro shows for the same records:

  • Nullable columns — where VFP shows a blank or empty value on a nullable column, the API returns null. Where VFP shows a real value, the API returns the typed value. Both cases should match.
  • Currency fields — appear as a JSON string with exactly four decimal places (e.g. "18.0000"), not a bare number.
  • General/OLE fields — do not appear at all. General/OLE (G) and Picture (P) columns are omitted from both the schema endpoint and row responses; you will not see them in the output. Read or write OLE/Picture data directly through FoxPro.
  • Autoincrement integers — read back as a plain JSON integer. Ninja assigns the next autoincrement value automatically on insert; do not include the column in your POST body. The schema endpoint marks these columns with "autoincrement": true.

System Requirements

ComponentRequirement
Operating SystemWindows Server 2012 R2 or later (primary). Linux and macOS supported for development and cloud-edge use.
Architecturex64 (AMD64)
Disk space50 MB for the binary + audit log growth (typically 1–10 MB/day depending on write volume)
Memory64 MB minimum; 256 MB recommended for production with large table scans
NetworkTCP port of your choice (default: 8080). HTTPS termination via a reverse proxy or Cloudflare Tunnel is recommended for non-loopback access.
FoxProFoxPro 2.x or Visual FoxPro 6–9. The data directory must be accessible from the machine running Ninja (local drive or UNC path).
ℹ️

Ninja is a single self-contained executable. There is no installer, no runtime to install, and no dependencies beyond the binary itself.

Download

Contact sales@foxproninja.com to receive a licensed binary for your platform. Each release includes:

  • ninja.exe — the server binary (Windows x64)
  • ninja-linux — Linux x64 build (for cloud-edge or development)
  • config.example.yaml — annotated configuration template
  • THIRD_PARTY_LICENSES.txt — open-source attribution notices

Place the binary in a folder of your choice — alongside your FoxPro application or in a dedicated directory such as C:\F8Labs\Ninja\.

Configuration File

Ninja is configured by a YAML file. By default it looks for config.yaml in the current directory. Pass a different path with the -c flag:

shell
ninja.exe -c C:\F8Labs\Ninja\config.yaml

A minimal configuration for read-only evaluation alongside live FoxPro:

config.yaml — minimal read-only
# Path to the folder containing your DBF / DBC files
data_dir: C:\AccountingData\appdata

# Bind to loopback only while evaluating
bind_addr: 127.0.0.1:8080

# Read-only mode: GET requests only, no writes
read_only: true

# Require a Bearer token even in read-only mode
auth:
  static_token: change-me-to-a-long-random-string

A full production configuration with per-scope tokens:

config.yaml — production
data_dir: C:\AccountingData\appdata
bind_addr: 0.0.0.0:8080

# Remove read_only (or set to false) to enable writes
read_only: false

# Expose only the tables you want visible over the API
expose_tables:
  - apvend
  - apbatch
  - glacct

# Per-scope tokens: read, write, admin
auth:
  static_tokens:
    read:  token-for-reporting-ui
    write: token-for-write-access
    admin: token-for-admin-ui

# CORS — required if a browser UI calls the API directly
cors:
  allow_origins:
    - https://yourapp.example.com

# Aggregate YAML definitions directory
aggregates_dir: C:\F8Labs\Ninja\aggregates

# Branding (shown in the admin UI header)
branding:
  service_label: Acme Corp

# Counter backend (use registry when running alongside FoxPro/AM)
counters:
  backend: registry

Configuration Reference

KeyTypeDefaultDescription
data_dirstringrequiredPath to the directory containing your DBF files (and optionally a .dbc file).
bind_addrstring127.0.0.1:8080TCP address to listen on. Use 0.0.0.0:8080 to accept connections from other machines. Auth is required for non-loopback addresses.
read_onlyboolfalseWhen true, all POST/PATCH/PUT/DELETE requests are rejected. Reads continue normally.
expose_tables[]stringall tablesWhitelist of table names to expose. Omit to expose all discovered tables.
aggregates_dirstringDirectory of YAML aggregate definitions. If omitted, /aggregates routes are not mounted.
auth.static_tokenstringSingle bearer token granting all scopes. Use this for simple single-client setups.
auth.static_tokensmapMap of scope → token. Valid scopes: read, write, admin. Takes precedence over static_token.
auth.allow_public_accessboolfalseSkip auth enforcement (for use behind Cloudflare Access or similar external auth). Disables the bind-address + CORS safety checks.
cors.allow_origins[]string[]List of allowed CORS origins for browser requests. Wildcard (*) is rejected unless allow_public_access is true.
counters.backendstringfilefile (standalone JSON store) or registry (reads/writes appreg01.dbf — required when running alongside a FoxPro application that uses a DBF counter registry).
counters.registry_filestringappreg01.dbfFilename of the counter registry DBF, relative to data_dir. Used only when backend: registry.
branding.service_labelstringNinjaLabel shown in the admin UI header and server logs.
log_levelstringinfoStructured log verbosity: debug, info, warn, error.

Starting the Server

shell (Windows)
# Start with default config.yaml in current directory
ninja.exe

# Start with an explicit config path
ninja.exe -c C:\F8Labs\Ninja\config.yaml

# Print version
ninja.exe -version

# Print third-party license notices
ninja.exe -credits

On startup, the server logs a summary of what it found — table count, DBC status, loaded aggregates, and any CDX tags it cannot maintain. Review this output before going live.

The server accepts SIGINT (Ctrl+C) and SIGTERM for graceful shutdown. In-flight requests are given up to 65 seconds to complete before the process exits.

Verifying It Works

Hit the liveness endpoint — no token required:

shell
curl http://127.0.0.1:8080/health
response
{ "status": "ok" }

Then fetch the full diagnostic with your admin token:

shell
curl -H "Authorization: Bearer <admin-token>" \
     http://127.0.0.1:8080/health/details

The response shows the table count, DBC status, loaded aggregates, and any CDX tags the engine cannot maintain. A healthy installation looks like:

response
{
  "status":   "ok",
  "tables":  185,
  "has_dbc": true,
  "unsupported_defaults": [],
  "unsupported_rules":    [],
  "stale_on_write_tags":  []
}

Non-empty stale_on_write_tags is not an error — it means a small number of CDX tags use expressions the engine can't maintain (typically STR()-based date arithmetic keys). FoxPro will rebuild them correctly; the API simply cannot keep them current during its own writes. See Health & Diagnostics for a full explanation of both fields.

Running as a Windows Service

For production deployments on Windows, run Ninja as a service so it starts automatically and restarts on failure. We recommend NSSM (the Non-Sucking Service Manager), a free utility that wraps any executable as a Windows service.

PowerShell (run as Administrator)
# Install NSSM from nssm.cc, then:
nssm install ninja "C:\F8Labs\Ninja\ninja.exe"
nssm set ninja AppParameters "-c C:\F8Labs\Ninja\config.yaml"
nssm set ninja AppDirectory "C:\F8Labs\Ninja"
nssm set ninja Start SERVICE_AUTO_START
nssm start ninja
⚠️

A graceful SIGTERM allows in-flight requests to finish (up to 65 seconds). If the service manager sends a hard kill (e.g. Windows Update restart), any aggregate operation in progress will be incomplete. See the Audit Log section for how to detect and recover from a torn write.

Authentication

All requests to a secured server must include a Bearer token in the Authorization header:

HTTP header
Authorization: Bearer your-token-here

Three permission levels (scopes) exist:

ScopeGrants access to
readGET on all table and aggregate endpoints
writeEverything in read, plus POST / PATCH / PUT / DELETE on tables and aggregates
adminEverything in write, plus the Admin UI and /health/details

Scopes are cumulative — an admin token also has read and write access. Use separate tokens for each scope to limit what a compromised client can do: give a reporting dashboard only the read token.

Error responses

HTTPCodeMeaning
401invalid_tokenToken missing or not recognised
403scope_requiredToken valid but insufficient scope for this endpoint

Tables — Layer 1

Every DBF table in your data directory (subject to expose_tables filtering) is available under /tables. All responses use lowercase snake_case JSON keys; column names in ?where= filters must be uppercase to match the DBF schema.

List tables

GET/tables
Scope: read
response
{
  "items": [
    { "name": "apvend", "long_name": "AP Vendor Master", "row_count": 412 }
  ]
}

Describe a table

GET/tables/{name}
Scope: read

Returns column names, types, lengths, and nullability. Use this to discover what fields are available before querying.

Insert a row

POST/tables/{name}/rows
Scope: write
request body
{ "CVENDNO": "000999", "LNAME": "New Vendor", "LACTIVE": true }
response — 201 Created
{ "key": "AAAAAAAB" }

The returned key is an opaque row handle. Use it for get/update/delete. Do not use it as a business key — use the table's own identifier column (e.g. CVENDNO) for that.

Get a row

GET/tables/{name}/rows/{key}
Scope: read

Update a row

PATCH/tables/{name}/rows/{key}
Scope: write

Partial update — only the fields you include are changed. PUT is accepted as an alias with identical semantics.

Delete a row

DELETE/tables/{name}/rows/{key}
Scope: write

Soft-deletes the row (sets the DBF deletion flag). The row is hidden from all subsequent API responses. FoxPro's PACK command removes soft-deleted rows permanently.

Field types in API responses

All fields are returned under their DBF column name, lowercased and converted to snake_case. The JSON type depends on the VFP field type:

VFP typeJSONNotes
Character (C), Varchar (V)stringTrailing spaces trimmed. Nullable variant returns null when the VFP null flag is set.
Memo (M)stringFull text content from the .fpt file. Can be arbitrarily long — do not assume it fits in a header or a log line. Returns "" if the FPT file is absent.
General / OLE (G), Picture (P)omittedThese columns carry raw binary blobs that have no JSON representation. They do not appear in schema responses or row responses — the column simply isn't surfaced by the API. Use FoxPro directly to read or write OLE / Picture data.
Numeric (N), Float (F), Double (B)numberNullable variant returns null.
Integer (I)number (integer)Autoincrement columns are assigned automatically by Ninja on insert — do not include them in the POST body. The schema endpoint reports "autoincrement": true for these columns. Nullable variant returns null.
Currency (Y)stringExactly four decimal places in canonical form (e.g. "18.0000"). Stored in VFP as a fixed-point int64 × 10⁻⁴; the string form preserves precision through JSON encode/decode.
Date (D)string (YYYY-MM-DD)Nullable variant returns null.
DateTime (T)string (ISO 8601)
Logical (L)boolean
⚠️

The FPT file must travel with the DBF. Memo fields store their data in a companion .fpt file with the same base name as the DBF. If the FPT is missing or inaccessible, memo fields return empty strings. Ninja logs a warning at startup for any DBF that has memo columns but no FPT file. General/OLE (G) and Picture (P) columns are omitted from the API entirely regardless of FPT availability.

Querying & Filtering

GET/tables/{name}/rows?where=COL:op:value
Scope: read

The ?where= parameter accepts one or more predicates comma-separated. All predicates are AND-ed. Column names must be uppercase.

OperatorExampleMeaning
eqCVENDNO:eq:000123Equal
neLACTIVE:ne:TNot equal
ltNAMOUNT:lt:1000Less than
leDDATE:le:2026-12-31Less than or equal
gtNAMOUNT:gt:0Greater than
geDDATE:ge:2026-01-01Greater than or equal
inCSTATE:in:TX|CA|NYValue in pipe-separated set
is_nullDMEMO:is_nullField is null / empty
eq:nullNQTY:eq:nullField is null — equivalent to is_null, works for any column type
ne:nullNQTY:ne:nullField is not null
likeLNAME:like:Acme%Pattern match (% = any, _ = one char)

Combining predicates (AND):

example
GET /tables/apbatch/rows?where=CVENDNO:eq:000123,LACTIVE:eq:T

Sort results with ?order_by=COLNAME (repeatable for multi-column sort):

example
GET /tables/apvend/rows?order_by=LNAME&order_by=CVENDNO

Aggregates — Layer 2

Aggregates are named operations that combine multiple tables, enforce business rules, allocate sequential numbers, and journal their changes atomically. Use aggregates — not raw table inserts — for any record that participates in a counter sequence (bills, invoices, payments, GL entries).

List aggregates

GET/aggregates
Scope: read

Returns all loaded aggregates and a list of any that failed validation at startup.

Create a resource

POST/aggregates/{name}
Scope: write
example — create a bill
POST /aggregates/bill
{
  "vendor_no":   "000123",
  "amount":     1250.00,
  "due_date":   "2026-07-15",
  "description": "Office supplies"
}

HTTP 201 Created
→ batch number allocated from appreg01.dbf
  vendor FK verified
  header + detail rows written in one journal
  CDX indexes updated

Dry-run before committing

POST/aggregates/{name}/dry-run
Scope: write

Same request body as Create. Returns the planned writes and counter preview without committing anything. Use this to validate input before going live.

Read an assembled resource

GET/aggregates/{name}/{id}
Scope: read

Returns the full assembled resource — header joined with all detail lines — for a given business key (e.g. batch number, invoice number).

Update a resource

PATCH/aggregates/{name}/{id}
Scope: write

Partial update. Only the fields you supply are changed. PUT accepted as an alias.

Delete a resource

DELETE/aggregates/{name}/{id}
Scope: write

Soft-deletes all rows associated with the resource per the aggregate's delete plan (cascade).

Admin UI

When the admin option is enabled in the server, a browser-based interface is available at /admin. It requires an admin-scoped token.

The admin UI provides:

  • A searchable list of all exposed tables with row counts
  • Per-table data browsing with sorting and filtering
  • Aggregate definition viewer
  • Recent audit log entries

The admin UI is served from the binary itself — there are no external files to deploy.

Health & Diagnostics

Two health endpoints are available:

EndpointAuthUse
GET /healthNoneLiveness probe. Returns {"status":"ok"}. Safe for load balancers and uptime monitors.
GET /health/detailsadminFull diagnostic snapshot. See fields below.

Understanding /health/details

A healthy installation returns a response like this:

response
{
  "status":               "ok",
  "tables":              185,
  "has_dbc":             true,
  "stale_on_write_tags":  [],
  "pack_events":          []
}

stale_on_write_tags

Ninja maintains every CDX index tag on every write — but only when it can evaluate the tag's key expression in Go. A small number of tags use FoxPro functions that require a live VFP evaluator (typically STR()-based date arithmetic like STR(YEAR(date),4)+STR(MONTH(date),2,0)). These tags are detected at startup and listed in stale_on_write_tags.

What it means: After Ninja writes a row on a table that has stale tags, those specific index entries will be out of date until FoxPro next opens the table. FoxPro's own writes and REINDEXes correct them automatically. Every other index tag on the same table is maintained correctly.

What to do: Nothing urgent. Check the list at startup. If a stale tag is on a CDX that your FoxPro code SEEKs heavily, coordinate with the FoxPro side to run REINDEX after a write session. If Ninja is running read-only, stale tags are irrelevant — Ninja never touches the CDX.

ℹ️

Ninja logs every stale tag at startup with the table name and tag name. The log output and /health/details show the same list.

pack_events

FoxPro's PACK and ZAP commands physically remove soft-deleted rows and renumber every remaining record. Because these require an exclusive lock, they cannot run while Ninja holds the file open — they must be done during a maintenance window when Ninja is stopped.

On every restart, Ninja compares each table's current row count to the count it saw before shutdown. If the count dropped, it records a pack_events entry with the table name, the timestamp of detection, and the before/after counts.

What it means: Any cursor or recno reference issued before the PACK now points to a different row. Clients that cached a cursor should discard it and re-paginate from the beginning.

Recommended workflow after a maintenance PACK: restart Ninja → call /health/details → if pack_events is non-empty, notify any connected clients to reset their pagination state.

Audit Log

Every committed write — Layer-1 or aggregate — is appended to a daily JSONL file in a .ninja-audit/ directory inside data_dir. Each entry records the timestamp, operation type, table(s) affected, row key, and the requesting user's token scope.

The audit log is append-only and never modified by the server. It is suitable for compliance archiving and for diagnosing what happened after an unexpected result.

⚠️

The audit log grows indefinitely. Implement an OS-level log rotation policy (e.g. Windows Task Scheduler deleting files older than 90 days) to prevent unbounded disk growth.

FoxPro Legacy Development Best Practices

Running Ninja alongside a live FoxPro or Visual FoxPro application does not require changes to your FoxPro code — the two systems share DBF files at the OS level and coexist today. But certain FoxPro coding patterns, common in single-user or early multi-user applications, become friction points when a second process is reading and writing the same data. The practices below are worth reviewing if you have the ability to touch the FoxPro source code. None are mandatory; each is a trade-off.

These apply equally whether you are modernizing a custom FoxPro application or any other DBF-based system. Prioritize the items that match your most active tables and highest-traffic code paths.

Prefer Record Locks over Table Locks

FoxPro offers two locking scopes: RLOCK() (record-level) and FLOCK() (file/table-level). A table lock acquired with FLOCK() blocks every other process — including Ninja — from writing to that table until the lock is released. In a single-user app this is invisible; with Ninja active, a long-running FLOCK() stacks up Ninja write requests behind it, and a Ninja write in progress blocks the FoxPro side from acquiring the table lock.

What to do: Audit routines that call FLOCK() and convert them to RLOCK() on the specific records they need to modify. This is the most impactful single change for coexistence throughput.

Already using RLOCK()? You're in good shape. Record-level locks cooperate naturally with Ninja's own per-operation locking — only the specific rows being written are locked, and only for the duration of the write.

What to watch for: Code that loops over a table and calls FLOCK() once at the top, then processes every record, then calls UNLOCK(). This pattern holds a table lock for seconds or minutes during busy processing. Break it into per-record RLOCK()/UNLOCK() pairs, or batch the records into memory first and release the lock before processing.

Avoid Exclusive Opens During Normal Operation

USE tablename EXCLUSIVE takes an OS-level exclusive lock on the DBF file. No other process — including Ninja — can open or read the table until the exclusive session ends. FoxPro requires exclusive access for structural operations (adding fields, rebuilding indexes, running PACK), but many older applications use it out of habit for ordinary data entry.

What to do: Identify routines that open tables exclusively and ask whether they actually need it. Most data-entry and reporting code does not. Reserve exclusive opens for structural operations and run those during a maintenance window when Ninja is stopped.

⚠️

A FoxPro session that holds a table exclusive will cause Ninja to return errors for any request touching that table until the exclusive session closes. If you see intermittent 503 or timeout errors from Ninja, an exclusive open in the FoxPro code is a likely cause.

Common pattern to fix: A print or report routine that opens the table exclusively "to get a consistent snapshot." Replace it with shared access plus RLOCK() on the rows being read, or restructure to read the data into an array before printing.

Schedule PACK, ZAP, and REINDEX in Maintenance Windows

These three commands require exclusive table access (FoxPro enforces this — they will not run while any other session has the table open) and they change the physical layout of the DBF file:

  • PACK — physically removes soft-deleted rows and renumbers every remaining record. Any API cursor or recno reference from before the PACK now points to a different row.
  • ZAP — deletes every row instantly (equivalent to PACK after deleting all records). Faster than DELETE ALL + PACK, but equally disruptive to any live references.
  • REINDEX — rebuilds the CDX from scratch. Safe for the data, but invalidates the index temporarily and requires exclusive access.

Because FoxPro cannot acquire the exclusive lock while Ninja holds the file open, these commands will fail during normal API operation — which is protective. The risk is the restart-after-operation window: if you run PACK while Ninja is stopped and then restart Ninja without clearing outstanding client cursors, those cursors are now referencing wrong recnos.

ℹ️

Ninja detects PACK and ZAP automatically. On the first read after restart, the engine compares the current row count to the count it saw before shutdown. If it dropped, a pack_events entry appears on GET /health/details with the table, timestamp, and before/after counts. Check this after every maintenance window that included PACK or ZAP.

Recommended workflow: Stop Ninja → run PACK/ZAP/REINDEX → restart Ninja → check /health/details for pack_events → notify any API clients to discard their cursors and re-paginate from the start.

Reducing PACK frequency: PACK is necessary because FoxPro's soft-delete model accumulates tombstoned rows. If a particular table grows large with deleted records, consider whether the FoxPro code can reuse deleted slots (via RECALL + re-populate) rather than always appending new rows. Ninja's own deletes also soft-delete — they are not reclaimed until a manual PACK.

Assign Counter Ownership Clearly

Sequential document numbers — invoice numbers, batch IDs, check numbers, GL keys — are typically stored in a central counter table (often appreg01.dbf). Both FoxPro and Ninja may need to allocate new numbers. If both systems read and increment the same counter row without coordination, they will issue duplicate numbers.

The principle: For each document type, designate one system as the allocator. The other system should either call into the allocator or accept numbers from the user rather than generating them independently.

Practical options:

  • FoxPro allocates, Ninja accepts: The user enters or pastes the next number when creating a document through the Ninja UI. Ninja validates the number is not a duplicate but does not generate it. Simple and safe during a transition period.
  • Ninja allocates via the counter registry: Configure Ninja to use counters.backend: registry, which reads and writes appreg01.dbf with row-level locking using the same locking protocol FoxPro uses. FoxPro's counter routine and Ninja will serialize naturally.
  • Partition by prefix: Give FoxPro all numbers in one range (e.g. 1–49999) and Ninja another (50000–99999). Simple to implement but visible in the data; also requires configuring FoxPro to stay in its range.
⚠️

The default Ninja counter store (counters.backend: file) is an independent file that does not coordinate with FoxPro's counter table. Enable the registry backend before Ninja creates any documents that share a number sequence with FoxPro.

Always Use SET DELETED ON

FoxPro's default for a new session depends on the application's initialization code. Code written without explicit SET DELETED ON at session start may see soft-deleted rows as if they were live — an artifact of FoxPro's "the row is there, just flagged" model. This was rarely a problem in single-user apps; with Ninja deleting rows through the API it becomes more visible.

What to do: Add SET DELETED ON to your application's startup routine (typically the main program or environment setup screen). This makes FoxPro invisible to deleted rows in all subsequent commands, matching the behavior Ninja already enforces on its side.

Side effect to audit: Turning on SET DELETED ON in code that previously ran without it may cause reports or queries to return fewer rows if deleted records were being included in counts or aggregations. Review any code that sums, counts, or averages over a table after making this change.

Ninja always filters soft-deleted rows — they never appear in API responses regardless of the FoxPro session setting. The recommendation here is to make FoxPro consistent with what the API exposes.

Audit DBC Triggers and Stored Procedures

If your data directory includes a Visual FoxPro Database Container (DBC), any stored procedures, triggers, or rules defined there will fire on writes from the FoxPro application. They do not fire on Ninja writes — Ninja reads the DBC metadata for field defaults and RI rules but executes those rules in Go, not by invoking the FoxPro runtime.

This asymmetry is intentional — FoxPro stored procedures can only run inside the FoxPro runtime — but it has practical implications:

  • Audit triggers: If a DBC trigger sends an email, writes to a log table, or updates a summary table, those side effects will not happen on Ninja writes. Decide whether that behavior needs to be replicated in a Ninja aggregate.
  • Validation rules: DBC field-level rules (the RULE clause on a field) are read and enforced by Ninja's rule evaluator for supported expression shapes. If you have rules that use complex FoxPro expressions, check /health/detailsunsupported_rules at startup to see which ones Ninja cannot evaluate. Those rules will not block invalid writes from the API path.
  • Default values: DBC DEFAULT clauses for fields are applied by Ninja when columns are omitted from an insert. Check /health/detailsunsupported_defaults for any expressions the engine cannot parse.
ℹ️

Ninja's evaluation of field rules and defaults is designed to handle the common patterns found in standard VFP applications. The Migration Toolkit can convert complex trigger logic into Ninja aggregate steps that run explicitly on each API write.

Be Aware of Long BROWSE Sessions

FoxPro's BROWSE command opens an interactive grid over a table. BROWSE holds the table open in shared mode for its entire session and caches the row count at open time. During a long BROWSE session:

  • Rows appended by Ninja may not appear in BROWSE until the user scrolls past the original end-of-file or closes and reopens BROWSE.
  • Rows deleted by Ninja will show as marked-deleted (strikethrough) if SET DELETED OFF is in effect, or simply disappear if SET DELETED ON.
  • BROWSE itself does not block Ninja — it holds a shared handle, not an exclusive lock — so API operations continue normally.

What to tell users: Close and reopen BROWSE (or press Ctrl+W and reopen) if the row count looks stale after the API has been active. This is a cosmetic issue, not a data integrity issue — the underlying DBF is correct.

Avoid leaving BROWSE open overnight on a table that Ninja writes frequently. The shared file handle is not harmful by itself, but users returning the next morning to a BROWSE showing yesterday's data can be confusing. Consider building your FoxPro UI to open BROWSE on demand and close it when not in use.

Migration Toolkit Overview

The Migration Toolkit (included in the Professional and Enterprise tiers) converts your existing FoxPro screens and programs into aggregate YAML definitions that the Ninja engine can execute. It is the fastest path from a working FoxPro application to a fully-capable REST API.

The toolkit pipeline:

  1. Scan — discovers all .sc2 (screen) and .prg (program) files in your FoxPro project.
  2. Parse — a native tokenizer extracts the structure and field references from each file without executing any FoxPro code.
  3. Ground — every field reference is validated against your live DBF schema, eliminating hallucinated column names.
  4. Convert — an LLM generates a YAML aggregate draft for each screen or program, using a vocabulary constrained to the transforms and validators the engine supports.
  5. Validate — each draft is loaded through the engine's YAML validator. Drafts that fail validation are automatically repaired before being saved.
  6. Emit — drafts are written to a review directory for human approval before promotion to the active aggregates directory.
ℹ️

The toolkit sends FoxPro source code to an LLM API for conversion. This is disclosed before the first run and requires an explicit acknowledgment flag. Source code is not retained by the LLM provider beyond the request.

Running a Migration

shell
# Dry run — analyze sources, report what would be converted, no LLM calls
ninja.exe migrate --dry-run \
  --source C:\FoxProApp\screens \
  --data-dir C:\AccountingData\appdata \
  --out C:\F8Labs\drafts

# Full conversion run with a cost cap
ninja.exe migrate \
  --source C:\FoxProApp\screens \
  --data-dir C:\AccountingData\appdata \
  --out C:\F8Labs\drafts \
  --max-cost 25.00
FlagDescription
--sourceDirectory containing .sc2 / .prg files.
--data-dirYour live DBF data directory (for schema grounding).
--outOutput directory for generated YAML drafts.
--dry-runAnalyze and report without making any LLM API calls or generating files.
--max-costMaximum LLM API spend in USD. The run stops when this limit is reached.
--no-cost-capRun without a cost limit. Must be specified explicitly if --max-cost is omitted.
--includeAdditional .prg files to bundle for cross-file class resolution.

Reviewing Drafts

After the toolkit runs, review the YAML drafts in the output directory before promoting them to your active aggregates directory. Each draft file includes:

  • The source file it was derived from
  • A confidence score and any gaps the toolkit flagged
  • The full YAML aggregate definition ready to load

Drafts that pass validation can be copied directly to the aggregates_dir configured in your server config. The server picks them up on the next restart (or hot-reload if enabled).

Use POST /aggregates/{name}/dry-run against a running server to test a promoted aggregate with real data before relying on it in production.