VerteOffice · Vertebra 2026
One spreadsheet runtime for four ERP modules
- Role
- Software Engineer
- Timeline
- Jan 2026 – Jul 2026
- Location
- Bogotá, Colombia (Remote)
- Stack
- React
- TypeScript
- Univer
- GraphQL
- Apollo Client
- WebSockets
Problem
A grid only one module could use
Vertebra makes software for public-utility services, including an ERP for billing and reconciliations. Its staff log their work hours in that ERP. The time-tracking module keeps them in four spreadsheet sheets: personal hours, general, authorizations, and pending tasks.
When I joined in January 2026, VerteOffice was a single component built on the open-source Univer spreadsheet library for that one module. Save, refresh, and filters were wired into the grid itself, so any other module that used it got all three whether it needed them or not.
Role
From one component to a shared runtime
I was a software engineer at Vertebra, working remotely from January to July 2026. A teammate built the first VerteOffice before I joined. I refactored it into a shared runtime: the architecture, the plugin system, per-sheet configuration, bulk editing, live updates in the grid, and the conflict screen. I moved time tracking onto it, built the payment notifications module, and wrote its API reference, module guide, and template.
Another engineer wrote the GraphQL subscriptions on the server. The runtime had to fit the existing React 18, TypeScript 4.2, Apollo, and Webpack 4 frontend.
Flow
Logging hours in the grid
Every VerteOffice screen shows real names and hours, so this flow is written out instead of shown. It follows the time-tracking module.
- 01
Open the sheet
The module opens with only the sheets, columns, and toolbar buttons its config asks for.
- 02
Type or paste
Type into cells, pick from dropdowns, or paste rows from Google Sheets. A Colombian date like 3/2/2026 becomes 2026-02-03. Invalid hours stay in the grid, marked, instead of disappearing.
- 03
Edit many rows
Select rows and apply one change to all of them from the right-click menu.
- 04
Review and save
Save lists every row it will create, update, and delete before sending anything. Rows with errors block the save until they are fixed.
- 05
Resolve conflicts
If someone else saved the same row first, that row stops and a screen shows your values beside theirs. Keep yours or discard them, one row at a time.
- 06
See other people's edits
Rows other people save appear without a refresh. Rows you are still editing are marked stale instead of overwritten.
Decisions
Modules declare what they need
Plugins and one config file per module
- Instead of
- Save, refresh, and filters built into every grid
- Because
- A module that never saves should not show a Save button, and payment notifications is read-only. The goal I wrote down in February was that adding a module would never mean editing VerteOffice.
A version on every row, and a conflict screen
- Instead of
- Blind overwrites between people editing the same row
- Because
- A save carries the row version it started from. If the server has a newer one, that row stops and the person saving decides, so no save silently replaces a newer one.
Live updates over WebSockets
- Instead of
- The manual refresh button
- Because
- The goal for the realtime phase was to replace manual refetching as the main way people see fresh data.
One filtered query per sheet
- Instead of
- Cursor pagination
- Because
- The product required Univer's filters to keep working inside the grid. Loading the active sheet first kept the data volume manageable.
Outcome
Four modules on one runtime
Four internal modules run on VerteOffice, each with its sheets, validation, and toolbar in its own config file.
I timed the same task in the old flow and in VerteOffice.
12 → 2 min
The same task, timed by me before and after