Writing

Why I Open-Sourced a Square Integration for Spree Commerce

August 14, 2026 · Amit Solanki

This is Part 2 of a four-part series on spree-restaurant, a Spree Commerce platform built so Square — not a second admin UI — owns the menu. Part 1 covers the platform architecture; this one is about a single piece of that project: why the Square integration inside it ended up as its own open-source gem, and what shipping it responsibly actually took. Part 3 covers DoorDash delivery dispatch, and Part 4 covers the RAG menu chat assistant.

Code’s here: github.com/amitkssolanki/spree_square · rubygems.org/gems/spree_square.

Why I built this

A restaurant client needed online ordering. The obvious path is to bolt an e-commerce platform on top and manage the menu there — except this restaurant, like most, already runs its entire operation through Square: prices, specials, sold-out items, the kitchen printer, all of it. Adding a second place to manage the menu doesn’t just double the work, it guarantees drift. Someone updates a price in Square and forgets the website, or marks something sold out at the counter without touching the online store, and now a customer’s ordering food that doesn’t exist.

The fix I built treats Square as what it already functionally is: the source of truth. The Spree Commerce storefront mirrors the menu instead of maintaining its own copy, and completed online orders get pushed back into Square as paid kitchen tickets — so staff never have to look at two systems to know what’s happening.

Partway through building it, it became clear the piece doing all of this — the sync engine itself — wasn’t actually specific to this one restaurant. It didn’t know anything about the client’s business beyond “here’s a Square account, here’s a Spree store.” That’s usually the sign something is worth pulling out and sharing rather than leaving buried inside one client’s codebase, so I did: spree_square, a standalone, MIT-licensed extension anyone running Spree can install.

What it actually took to ship it responsibly

Publishing an internal tool for other people to install isn’t just uploading the code — a few things had to happen first, and each one is worth naming honestly rather than glossing over.

Packaging discipline. The version living inside the client project also carried demo data — a full sample restaurant menu with real stock photos, used for local development and demos, adding up to double-digit megabytes. Nobody installing “a Square integration for Spree” wants that bundled into their app. The packaging had to be rebuilt so only what an installer actually needs gets shipped, while the demo content stays in the source repository as a working example for anyone who wants to see the whole thing in action.

A real security review, not a quick glance. Before anything went to a public host, I assumed something had probably ended up somewhere it shouldn’t over months of iteration — a token pasted into a comment during debugging, a real account ID hardcoded for a quick test. So I checked: the entire commit history, not just the current state of the code, searched for the shape of every kind of credential and account identifier that had touched the project. It turned up exactly two things, both harmless — a contact email in the package metadata, which is standard practice, and attribution metadata embedded in two stock photos, which belongs to the photographers, not the project. That’s the outcome you want from a review like this: not an assumption that it’s clean, but confirmation.

Decoupling from the client project. The extension lived inside the client’s own codebase. Splitting it into its own repository and having the client project reference it as an external dependency, pinned to a specific released version, meant the two could evolve independently — the client gets updates deliberately, on their own schedule, instead of automatically inheriting whatever changes next.

Actually publishing it, with the ordinary friction that comes with a first release: expired credentials that needed regenerating, two-factor confirmation on the actual publish step — exactly the kind of security friction you want on something other people are going to install and run. Small annoyances, but worth expecting rather than being caught off guard by mid-release.

Closing real test-coverage gaps before shipping, not after. Before the first release went out, I went through it honestly: did the automated tests actually cover the parts that would hurt someone in production if they broke — the trigger that pushes an order the moment a customer completes checkout, and what happens when a background sync job fails and needs to retry. It didn’t, not fully. That got fixed first.

And then, within days of the first release, proof the whole exercise was worth it. A real gap turned up — the connection to Square could take payments but not check their status afterward, an oversight nobody had hit until something actually needed it. Because this was now a properly versioned release rather than a moving target, fixing it was a normal, deliberate update: bump the version, explain what changed and why, ship it. Anyone using the extension knew exactly what changed and when. That’s the entire point of doing this properly instead of just open-sourcing a folder — updates become something you can trust, not something you have to double check.

What this makes possible

For a restaurant or retailer already running Square, this closes the gap between “we have a POS” and “we have online ordering” without creating a second system to maintain:

  • One menu, one place to manage it. Staff keep doing exactly what they already do in Square. The website follows automatically, usually within seconds of a change.
  • No double-entry, ever. A price change, a new item, an 86’d dish — none of it needs to be re-entered anywhere.
  • Orders show up where the kitchen already looks. A completed online order becomes a paid ticket in Square, same as a walk-in sale, with no new screen for staff to learn.
  • Status flows back automatically. Advance or cancel an order at the counter, and the customer’s order status reflects it — no one has to remember to update the website by hand.
  • It’s built to be trusted unattended. A nightly safety-net pass catches anything a missed notification might have dropped, and a failed order push — the one failure mode that actually matters, since it means a customer paid and the kitchen never found out — is retried automatically and escalated to a person if it still doesn’t go through.

How this can help others

This isn’t useful only to the restaurant it was originally built for.

If you’re a restaurant or retail business already running Square and considering online ordering, this removes the single biggest piece of custom engineering — the sync between your POS and your storefront — as a starting point rather than something built from zero.

If you build or maintain Spree stores for clients, this is a working, tested, open-source foundation for a Square integration instead of a blank page. It’s free to use, modify, and extend under the MIT license.

If your business runs on a different POS entirely — Toast, Clover, Lightspeed, or anything else — the pattern this project proves out (the POS stays the source of truth, changes sync in near real time, completed orders flow back in as paid tickets, status syncs both ways) isn’t specific to Square. It’s a template for the same integration against a different system.

If you want this for your own store

The extension is public and free to use — MIT licensed, on GitHub and RubyGems, installable in any Spree store.

If you’re running a business on Square and want a storefront that actually stays in sync with it, if you need a Spree solution built around this integration, or if you run a different POS system entirely and want the same thing built for it, reach out — [email protected] or @amitkssolanki on GitHub.

Working through something similar?

← All writing