Case Study: Serving Nightly CSV Exports and a SQLite Inventory as a JSON API
This case uses the examples/legacy-exports folder that ships in every download archive, so you can run each step yourself. The shop app is a stand-in. The commands and output below come from a real run of the downloaded aftermarket 0.9.0 on Linux, with the inventory loaded into a real database file (sqlite3 inventory.db < inventory.sql) and served from a copy of the config, bridge-db.yaml, that points at it.
The situation
An old shop app has no API. Every night it writes its orders to exports/orders-YYYY-MM-DD.csv, with a free-text note column the staff use for things customers shouldn’t see. Its stock lives in a SQLite database, products with a photo blob each. A new dashboard needs unpaid orders and stock levels, and nobody wants to touch the app.
bridge.yaml describes both, typing the order columns and leaving the note and the photo out:
sources:
- id: orders
csv:
folder: exports
pattern: "orders-*.csv"
key: id
columns: {id: integer, total: number, paid: boolean, placed: date}
exclude: [internal_note]
- id: products
sqlite: {db: inventory.db, table: products}
key: sku
columns: {price: number, stock: integer, discontinued: boolean}
exclude: [photo]
The endpoints are /orders (filtered by paid and placed), /orders/{id}, and /products/{sku} with a couple of fields renamed.
Checking it first
$ aftermarket bridge check --config bridge.yaml
bridge.yaml: 2 source(s), 3 endpoint(s)
source orders: 4 row(s), 5 column(s) from orders-2026-10-21.csv
source products: 3 row(s), 5 column(s) from inventory.sql
/orders -> orders (list, filters paid, placed)
/orders/{id} -> orders (one id)
/products/{sku} -> products (one sku)
ok
The example’s own config reads the inventory dump, inventory.sql. Of the two exports in the folder, the 21st is the newest by name. bridge test with the example’s fixture passes 8 of 8.
Asking for things
$ curl -s 'http://127.0.0.1:18095/orders?paid=false'
{"items":[{"id":1004,"customer":"Turing, Alan","total":8.5,"paid":false,"placed":"2026-10-21"}],"total":1,"limit":100,"offset":0}
$ curl -s 'http://127.0.0.1:18095/products/BK-001'
{"sku":"BK-001","title":"Notes on the Analytical Engine","price":24.5,"in_stock":12,"discontinued":false}
8.50 in the file came back as the number 8.5, no as false, and the comma in “Turing, Alan” survived its quotes. Asking wrongly gets told why:
$ curl -s 'http://127.0.0.1:18095/orders?paid=maybe'
{"error":"paid must be true or false"}
$ curl -s 'http://127.0.0.1:18095/orders?customer=Ada'
{"error":"unknown query parameter \"customer\""}
$ curl -s -X DELETE http://127.0.0.1:18095/orders/1001
{"error":"only GET and HEAD are answered"}
The next night’s export
The app writes the next day’s file in two pieces, a second apart. Halfway through, the bridge is still serving the 21st:
$ curl -s 'http://127.0.0.1:18095/orders?limit=1'
{"items":[{"id":1001,"customer":"Ada Lovelace","total":39.8,"paid":true,"placed":"2026-10-19"}],"total":4,"limit":1,"offset":0}
Right after the last line is written, still the 21st. A few seconds later, once the file has been left alone, the 22nd is served:
$ curl -s 'http://127.0.0.1:18095/orders?placed=2026-10-22'
{"items":[{"id":1005,"customer":"Margaret Hamilton","total":120.0,"paid":false,"placed":"2026-10-22"}],"total":1,"limit":100,"offset":0}
120.00 is a number column, so it comes back as 120.0, not as the integer 120.
A broken export
The night after, something goes wrong in the app and the 23rd’s file has twelve in the total column. The bridge refuses it and keeps the 22nd. From the log, trimmed:
{"event":"kept","rule":"orders","reason":"line 2, column total: not a number; still serving the last good snapshot"}
That line appears in the log once, not on every check, and it names the line and column without repeating the value. /orders/1005 keeps answering as before.
Stock changing underneath
The app updates the stock in its database while the bridge is running:
$ sqlite3 inventory.db "UPDATE products SET stock = 11 WHERE sku = 'BK-001'"
$ curl -s http://127.0.0.1:18095/products/BK-001
{"sku":"BK-001","title":"Notes on the Analytical Engine","price":24.5,"in_stock":11,"discontinued":false}
A database is read in one consistent query, so it doesn’t need to settle. The new value is there at the next check.
A copy for later
bridge convert writes one source’s snapshot to a file, for a spreadsheet or a one-off import:
$ aftermarket bridge convert --config bridge-db.yaml --source products --out products.json
wrote products.json: 3 row(s) of products from inventory.db
It refuses to write over anything the bridge reads, including a name in the exports folder that the pattern would pick up the next night. The AfterBridge reference has the full format.