Skip to main content

        Mini and Mighty: A Full Rewrite - Featured image

Mini and Mighty: A Full Rewrite

Where Things Left Off

In a previous post I wrote about building a product management portal for Mini & Mighty. The core problem was straightforward: product data living across multiple systems, updated manually in each. The solution was a single portal sitting in front of AWS, syncing changes outward through a Lambda pipeline. It worked, it cost next to nothing, and the team used it.

That was the end of the story, until it wasn’t. The scope grew. They needed a customer-facing portal, order management, Xero invoicing, customer data from their wholesale contact group, and real-time notifications when customers placed orders. None of that was unreasonable, but it was enough to justify starting fresh rather than extending a codebase that wasn’t designed for it. The original was a product management portal. This needed to be a commerce platform.

What the Original Got Right

Before getting into what changed, it’s worth being clear on what didn’t. The event-driven pipeline for CSV imports held up. A file uploaded to S3 triggers a Lambda that parses rows and queues one message per row onto SQS. A second Lambda consumes the queue and fans out updates to WooCommerce, Xero, and DynamoDB in parallel. That pattern was sound in the original and it’s still sound now. The only difference in the rewrite is that it sits inside a properly structured monorepo instead of a single Next.js project with Lambdas bolted on the side.

The decision to lean on Lambda and DynamoDB within the AWS free tier also held. The new system is considerably more capable and the running cost hasn’t moved meaningfully. That matters because it means architectural decisions were made on the basis of what the system actually needed, not on the basis of what the infrastructure was already paying for.

Turborepo and the Internal Package Problem

The original repo had a shared code problem. Lambdas and the portal needed access to the same types and utilities, and the solution at the time was loose enough that it became a maintenance liability. The rewrite introduced Turborepo as the monorepo tool, which brought proper workspace management and task orchestration, but the more important decision was how to handle shared code.

Rather than publishing internal packages to a registry or duplicating types across workspaces, the repo uses the internal package pattern: packages under src/packages/ export .ts source files directly via their exports field. No build step, no compilation artefacts, no version management. Next.js apps list them in transpilePackages and TypeScript resolves paths directly to source. It’s the right amount of infrastructure for the problem.

There are four internal packages. domain contains the core types (Product, Customer, Order, OperationalLog), mappers for WooCommerce and Xero API responses, and builders that generate DynamoDB keys and set TTLs. api wraps the WooCommerce and Xero HTTP clients. aws wraps the DynamoDB, S3, SQS, and SNS clients. ui contains shared React components, the full portal theme system, and a structured logger factory built on Pino.

Having ui as an internal package solved a specific problem: two portals targeting different audiences but sharing the same visual language and layout chrome. Without it, the theme would be duplicated and would diverge. With it, both portals import the same sidebar, header, user menu, and nav components. Each portal only defines its own nav links and portal label, and wires those into the shared layout via props.

Although I can’t and won’t share the code you can see some of the high-level folder structure below.

Project Snapshot

The Order Pipeline

This is the biggest addition and the one that required the most thought. Orders in Mini & Mighty’s context are wholesale orders placed against Xero. An authorised invoice gets created, the customer gets emailed by Xero, and the system tracks the invoice status over time.

The portal doesn’t call Xero directly. Order placement goes through SQS. The business portal writes a PlaceOrderRequest message onto an orders queue. A Lambda consumes it and works through four steps: resolve current unit prices from DynamoDB, resolve the customer’s Xero contact ID, create the Xero invoice, then save the Order record to DynamoDB. The order is only persisted if the Xero call succeeds. If the invoice creation fails, the Lambda writes a failure log and a notification record for the business portal and returns without saving.

The same decoupled approach applies to order updates and deletions. The portal sends a DeleteOrderRequest or UpdateOrderRequest onto a separate queue. A processor Lambda routes by message type: deletions void the Xero invoice before removing the Order from DynamoDB; updates fetch the current invoice status from Xero and sync it back.

Status tracking is driven by a scheduler. Four times a day, an EventBridge rule triggers a Lambda that queries a sparse GSI on the DynamoDB table. Only active orders appear in this index, so the query only hits orders that are still in a transient state (AwaitingApproval, OrderPlaced, AwaitingPayment). One UpdateOrderRequest message per active order goes onto the updates queue. This keeps the status loop asynchronous and cost-free; there’s no background process holding a connection open.

Customer Data

The customer portal needed customer records in DynamoDB, which meant pulling them from Xero’s “Wholesale Customers” contact group. A refresh scheduler Lambda runs four times a day, scheduled to avoid simultaneous Xero API calls. It fetches the group, pulls full contact details in batches of 100 (Xero’s limit per request), maps each contact to a Customer record with a SourceCustomer array, and upserts to DynamoDB. Outstanding balances come through as part of the contact payload, which is useful context on the business portal’s customer view.

The customer portal itself is a second Next.js app at src/apps/customer-portal, running on a separate Vercel deployment. It shares authentication (Auth0 via next-auth), the same UI package, and the same domain types as the business portal. A customer sees their own orders, a product catalogue, and their account details. All data fetching goes through the same DynamoDB table, with the portal only reading records belonging to the authenticated customer.

Customer Portal

Notifications

When a customer places an order via the customer portal, the business team needs to know. When an order is placed successfully, the order processor writes a Notification record to DynamoDB. If the order fails, a failure notification is written instead. There’s also a separate SNS topic for cases where a notification should fire without going through the full order path — a dedicated Lambda subscribes to it and writes the notification record directly.

Business Portal Customers

On the business portal side, a notification bell component polls a notifications endpoint every 10 seconds when the tab is visible. When the user dismisses a notification, the record is removed from DynamoDB. There’s no WebSocket, no SSE stream. Polling at that interval is accurate enough for the use case, and the visibility check means a tab left open in the background isn’t hammering the API unnecessarily.

Lambda Decomposition

The original system had two Lambdas. The new system has eight. That’s not complexity for its own sake. Each one has a single clearly bounded job. The schedulers are responsible for fetching data and enqueueing work. The processors consume queues and do the actual work.

That boundary matters. A scheduler failing doesn’t affect a processor that’s already mid-flight. A processor backing up under load doesn’t starve a scheduler. The queues decouple timing entirely. Each Lambda can be redeployed independently, tested in isolation, and configured with its own concurrency and retry settings.

The CDK stack provisions the whole infrastructure: DynamoDB single table with GSIs, an S3 bucket, SQS queues, an SNS topic, EventBridge rules, and all eight Lambdas with their IAM roles and event source mappings. Changes to Lambdas or shared packages trigger an automated CDK deploy via GitHub Actions on pushes to main. The portals deploy to Vercel as they did in the original.

UI Rework

Rewriting the codebase meant I could really spend time making the UI look and feel more user friendly.

The old one wasn’t the worst. I used Material UI last time to save time and get something usable deployed. This time I used tailwind css and created components tailor made and shared across both portals.

Comparing the two, this felt a much more slicker approach. I was asked a number of times for something more custom and although I could create and overwrite theming in Material UI, much of the user “gripes” with the look was very tied to Material’s look and feel, not colour changes.

To me it felt justified to move away from Material and the customer feels much more comfortable for it.

Customer Portal

Thinking Clearly at Low Cost

Something that doesn’t show up directly in the architecture but shapes how it was built: when the infrastructure costs almost nothing, there’s no pressure to consolidate services to reduce the bill. That sounds obvious, but it has a real effect on how you design. There’s no temptation to make a Lambda do two things because running two separate ones “seems wasteful.” The cost of running eight Lambdas at this scale is indistinguishable from running two.

That same freedom applied to the DynamoDB table design. A single table with composite keys and sparse GSIs is the idiomatic approach for this kind of access pattern, but it requires understanding the query patterns up front. There was time to think that through properly because the question was never whether to use something cheaper instead.

The original post made the point that well-architected solutions don’t have to be expensive. The rewrite reinforces it from the other direction: keeping the cost floor low buys the space to architect well, without every structural decision being second-guessed against a hosting invoice.

The Future

The approach of the turbo based monorepo, a single language, shared code and event driven architecture really lends to low cost solutions for SMBs, quick development, low infrastructure costs, easier to maintain smaller units of work and build upon the future.

Sometimes as a developer I architect with myself in mind and its easy to fall into that trap instead of thinking of what works best for my customers with their goals in mind (internal or external).

I’ve met a lot of other developers that insist on Docker, K8s, multiple environments, API driven, telemetry and massive logging systems. In an enterprise world this is all standard and more, but the result often matters more, and many smaller organisations don’t see a benefit or results of spending hundreds and thousands. Remember YAGNI. You don’t automatically need k8s.

I feel like I’ve achieved a middle ground of maintainable infrastructure vs costs, without trying to justify expenses where benefit isn’t always visible, but also in a way where I’ve not boxed the organisation in to growth. The best bit is that it wasn’t as time consuming as it appears.

There is a lot more detail in the real stack, more Lambdas, more queues, more everything AWS, but this condensed view gives a good idea of the flow.

Customer Portal

This solution seems simple and it is for good reason. They may outgrow this in a little while. The need for APIs, K8s and Elastic may come one day. Then again, it may not. I typically try to write for now and think of more immediate pain points. The code you write now is tech debt sooner or later so I don’t tend to get too attached to it.

SMBs have unique challenges and I often love developing ideas around these constraints. Its easy as a developer to be stuck in the enterprise world, with enterprise funding, developing enterprise applications.

This rewrite is now in the final stages of testing and user acceptance and now ready to be used. There are minor tweaks here and there still to do but these are very minor content tweaks.

The Future for me?

Although I’ve only discussed Mini and Mighty and small side projects, there has been other works going on I wish I could share.

For now I’m taking a break and looking back to more personal side work I can fully share, code and all! One or two are very small but potentially useful utilities but I’d love to build and share something a bit more impressive.

Maybe I’ll take a small break from development and share something more music related? It is both a music and tech blog!