Skip to content
heapbyte - A name of excellence

Architecture · 22 September 2026

The spreadsheet is not the problem: business-managed data as an integration boundary

The pricing lives in a Google Sheet. Not a copy of it, not an export — the sheet is where the decisions are made, and it has been that way since before there was a website. Rates per material, a multiplier that changes with volume, a column of exceptions somebody added for the accounts that get special terms. The proposal on the table is to move all of it into the store, where it belongs. The finance lead, who maintains it, has not said no. She has asked what she is supposed to use instead on the Tuesday of a price review.

7 min read
Written by the HeapByte engineering team

The spreadsheet is not the problem: business-managed data as an integration boundary

Shopify has somewhere to put this

The proposal is not naive, and it is worth saying what it gets right. Shopify has a real home for merchant-managed structured data. Metaobjects are custom data structures editable in the admin or through an app, readable from Liquid and the Storefront API, with a bulk editor for changing many at once and bulk operations for large writes.

For most of what a business wants to keep control of — size guides, shipping zones by region, a table of finishes — that is the correct answer, and connecting an external system to hold it instead would be an eccentric choice you would have to defend later.

So the question is not where data can live. It is what happens to this particular data when it moves.

Values move; models do not

A metaobject holds values. A spreadsheet, once it has been in use for a few years, holds a model: formulas, references between sheets, conditional rules, and at least one column whose purpose nobody can fully explain but which three other columns depend on.

Export the outputs and you keep the numbers and lose the reasoning. The finance lead's next price review has nowhere to happen. What follows is predictable: she rebuilds the model in a new sheet to do the review, then somebody types the results into the admin, and now there are two sources of truth and a monthly opportunity to update the wrong one. The migration succeeded and the business is worse off.

The position this article takes is that a spreadsheet the business genuinely reasons in should be left where it is and treated as an upstream system — not because spreadsheets are good infrastructure, but because the thing that makes it valuable is the part that does not survive the move. What would change my mind is the sheet being a dumping ground rather than a model: if it is a flat list of values with no formulas, it has no reasoning to lose, and metaobjects are better in every respect.

An informal interface needs a formal boundary

Deciding to keep it means accepting what it is: an interface with no type system, no required fields, no validation, and an editor who is thinking about pricing rather than about your parser. Cells get formatted as text. Someone types 1,250 because that is how a number looks. A column gets renamed to something clearer. A note goes in column H.

None of that is carelessness — it is what a spreadsheet is for. It does mean the reading side has to be stricter than it would be against an API, not looser.

js
/**
 * Accepts "1250" and "1250.5". Refuses "1,250", "1 250", "1250 kr" and "1.250,5".
 *
 * Every one of those is a number to a human and ambiguous to a parser: in some
 * locales "1.250" is one thousand two hundred and fifty. Coercing it picks a
 * locale on the merchant's behalf and is wrong silently, at the third decimal
 * place, in a price.
 */
function parseDecimal(raw) {
  if (!/^-?\d+(\.\d+)?$/.test(raw)) return undefined;
  return Number(raw);
}

export function parseRow(row, schema) {
  const out = {};
  const problems = [];

  for (const [column, rule] of Object.entries(schema)) {
    const present = Object.prototype.hasOwnProperty.call(row, column);
    if (!present) {
      problems.push(`${column}: column missing from the sheet`);
      continue;
    }

    const raw = String(row[column] ?? "").trim();

    if (raw === "") {
      // Blank is only a problem when the column is required. It is never zero.
      if (rule.required) problems.push(`${column}: required but blank`);
      continue;
    }

    if (rule.kind === "text") {
      out[column] = raw;
      continue;
    }

    const value = parseDecimal(raw);
    if (value === undefined) {
      problems.push(`${column}: ${JSON.stringify(raw)} is not a plain number`);
      continue;
    }
    if (rule.min !== undefined && value < rule.min) {
      problems.push(`${column}: ${value} is below the minimum of ${rule.min}`);
      continue;
    }
    out[column] = value;
  }

  if (problems.length > 0) {
    throw new Error(`row rejected — ${problems.join("; ")}`);
  }
  return out;
}
parseDecimal refuses more than it accepts, and the refusals are the point. “1,250” is one thousand two hundred and fifty in one locale and one and a quarter in another; “1.250” reverses the two. A parser that coerces those has picked a locale on the merchant's behalf and will be wrong silently, in a price, with nothing downstream to notice. The other half is reporting every problem in the row rather than the first — somebody is going to fix these in a browser tab with the sheet open beside it, and three round trips to learn about three columns is how a person stops using the validation and starts working around it.

The schema is the contract

The schema is the contract, and writing it down is most of the work. Which columns exist, what each one means, what is required, what range is sane. Once that exists, a bad row is a specific message naming a specific cell, and the person who typed it can fix it without anybody looking at a log.

Caching is not an optimisation here

The other thing the boundary buys is that the storefront stops depending on a third party being up.

If a product page reads the sheet at request time, then Google's availability is your availability and Google's latency is in your time to first byte. That is not a theoretical concern for a page a customer is configuring. So the values are pulled on a schedule, validated on the way in, and served from your own store — which means the storefront reads data that is minutes old and always there, rather than current and sometimes absent. For pricing inputs that change when somebody holds a review, minutes old is indistinguishable from current.

It also draws a clean line: the merchant owns the source values, and the application owns the formulas and the validation. Where the resulting price is then allowed to live, and how it is verified once a customer has it in a cart, is a separate question with its own answer.

What this does not do

It does not make the spreadsheet reliable. Nothing stops someone deleting a row, sorting a column independently of its neighbours, or pasting in a new tab structure. Validation catches malformed data, not wrong data — a rate of 4.2 where 42 was meant will pass every check and reach a customer.

It does not change who can edit it, and this is the one worth raising before anybody agrees to the approach. A spreadsheet that feeds a storefront is a production system, but it is shared like a spreadsheet — link access, a contractor added for a project two years ago, an inherited tab nobody audits. Admin access to Shopify is usually deliberate and reviewed; edit access to a sheet rarely is. Nothing in the integration fixes that, and the sheet should be locked down to the people who genuinely maintain it on the day it becomes an input.

It does not scale indefinitely. A worksheet is fine at hundreds of rows and unpleasant at tens of thousands, and the honest signal to migrate is when the sheet stops being something a person reads and becomes a database with the wrong interface.

And it does not remove the need for a schema conversation. Somebody has to agree what the columns mean and then not change them casually, which is a working agreement rather than a technical control. The validation makes a breach visible; it does not prevent one.

When this needs an engineer

Frequently it does not. If the data is a flat table of values, put it in metaobjects and use the admin — that is what they are for, and an integration would be a liability you chose. If the sheet changes twice a year, export it and paste it. If only one person uses it and they are happy to work in the admin, the question answers itself.

It needs engineering when the spreadsheet is load-bearing for how the business actually makes decisions, when the values behind a customer-facing calculation change often enough that a manual copy will drift, and when being wrong is expensive because the output is a price. That was the case for a merchant whose commercial logic lived in Sheets and whose storefront had to reflect it — the pricing integration sits with our other custom Shopify applications, and it is custom Shopify app development rather than a data migration, because the point was to leave the data where the business could keep using it.

Send us the store and the symptom.

Insights

Apply this to your store.

An audit turns the general principle into a specific list of changes, ordered by what actually pays back.