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.

  1. 01

    Open the sheet

    The module opens with only the sheets, columns, and toolbar buttons its config asks for.

  2. 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.

  3. 03

    Edit many rows

    Select rows and apply one change to all of them from the right-click menu.

  4. 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.

  5. 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.

  6. 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