nocake Start a project

Software that doesn't break.

nocake designs, builds and runs websites, web and mobile apps, and the backends behind them. From Łódź, for clients everywhere.

Left half of a face in dark cross-stitch: Tomasz Żełudziewicz Right half of the same face in coral cross-stitch: Piotr Żełudziewicz
Tomasz ŻełudziewiczCTO, architect and principal engineer

The people you talk to are the people who write the code.

Piotr ŻełudziewiczDirector of Engineering, Engineering Manager and Tech Lead

Where we can help.

Large, professional systems are our everyday work. Bring us the part that is hard, or the whole thing.

Not sure where yours fits? Tell us anyway.

Start a project

Work we can describe.

Case study

A billion-dollar monolith, taken apart

One PHP and Laravel monolith, behind a service earning a billion dollars a year. We did the whole move to microservices: the architecture, the event streaming, the data pipeline, the developer tooling, and the harder part, getting eight teams on board.

  • $1B a year through the service
  • 8 teams moved onto microservices
  • 12 bounded contexts

Finding the seams

A monolith doesn't come apart along its folders. We drew the new boundaries around the business: twelve bounded contexts, each able to change without asking the other eleven for permission.

Placing those lines was the real work: cut in the wrong place and two services end up changing together, which is a monolith with extra network calls. Once the boundaries held, the contexts stopped reaching into each other's code and started exchanging events, so each one only has to say what happened, not who needs to hear about it.

Kafka Monolith
Every service puts its own events on one stream. The others take a copy, and the original keeps moving.

Why Kafka

RabbitMQ SQS Chosen: Kafka

We didn't choose the broker out of habit. We put Kafka, RabbitMQ and SQS side by side, and two things settled it: a lot of data, and a lot of consumers. Kafka keeps events in a log instead of handing each one out once, so every consumer reads at its own pace, and a team that turns up next year can still start from the beginning.

One log, four readers, four positions. Nobody waits for anybody.
RequirementKafkaRabbitMQSQS
High data volumeBuilt for it: a partitioned logWorks, gets harder as throughput growsScales, but handled and priced per message
Many consumersConsumer groups read independentlyOne queue per consumerNeeds SNS and one queue per consumer
Reading history againRetained, replay from any offsetGone once consumed, in classic queuesGone once deleted
OrderingPer partitionPer queueFIFO queues only

Why events

REST Chosen: Event streaming

With REST, the caller has to know everyone who cares, and waits for them to answer. With events, a service says what happened and moves on. A new team subscribes, and nobody else changes a line.

RESTCallerServiceServiceServiceNew teamwaitingcode change EventsstreamServiceServiceServiceServiceNew teammoved onno change
Same new team, two very different amounts of work.

Why Avro

JSON Protobuf Chosen: Avro

JSON is friendly until a field changes and a consumer three teams away finds out in production. Protobuf was the closest call. Avro with Confluent Schema Registry gave us compact messages and a contract that gets checked on every schema change.

JSON{ }ServiceConsumerfinds out in production Avro{ }{ }ServiceSchemaRegistryConsumerbreaking change stops herecompatible, goes through
The same changed field. One finds out in production, the other at the check.

Into Snowflake

The data analysts already did their work in Snowflake, so nobody needed a new place to run their analysis. And the events now travelling through Kafka were ideal raw material for it: the activity of the business as it happened, structured and backed by a schema.

All that was missing was the link between the two. There was no off-the-shelf shortcut here, so we wrote a custom driver that carries the data from Kafka into Snowflake, where the analysts can take it further with the tools they know.

KafkaCustom driverSnowflakeAnalystseventswritten by usthe tools they know
The one missing link, and the part we had to write.

Production on a laptop

Twelve contexts are no use if nobody can run them. We built a complete local environment for developers, with production services mocked, on Docker, Node.js and Google zx, and with Confluent Schema Registry and Avro in the loop just as they are in production.

Docker12 bounded contextsKafkaSchema RegistryMockedserviceMockedserviceMockedservice
The whole system on one developer's machine, with the outside world mocked.

Eight teams had to believe it

The hardest migration wasn't in the code.

Eight teams don't change how they work because a diagram says so. We onboarded them, ran the training and kept explaining the why: what microservices are good at, where they hurt, and when they're the wrong answer. Nobody was sold a silver bullet, which is why it stuck.

Onboarded, trained and won over: all eight teams.
Where they're strong
  • Teams ship without waiting for each other
  • A failure can stay inside one service
  • Each part scales on its own
  • New consumers don't touch the producer
Where they hurt
  • A function call becomes a network call that can fail
  • Data is eventually consistent, not instantly
  • Debugging crosses service boundaries
  • There's a lot more to deploy, watch and pay for

Case study

100 GB anonymized: from 20 hours to 10 minutes

Developers sometimes need production data, but they can't be handed the records of hundreds of thousands of real customers. We built a system that takes a copy of the production database, replaces the personal data with fictional data, and leaves everything still joining up. The previous solution needed 20 hours for this. Ours needs 10 minutes.

  • 10 min for 100 GB and hundreds of millions of records, down from 20 hours
  • 3× a week
  • 99.98% success rate
  • 140 tables
  • 80 columns anonymized

See it work

customers
nameemailssn
Maria Lis[email protected]219-09-9999
Paul Reed[email protected]123-45-6789
orders
customeremailgeolocation
Paul Reed[email protected]39.95, -75.16
Maria Lis[email protected]52.23, 21.01

Sample data. The same colour means the same person, in both tables, before and after.

Seeds fall short, production is off limits

Day to day, the developers on this project work on a simplified database built from seeds. But even the best seed differs from what's really sitting in production, and sometimes that difference is exactly what matters. Direct access is out for obvious reasons: this is the data of hundreds of thousands of customers. What was needed was a database that behaves like production without holding their personal data.

DeveloperSeedsdiffers from productionProductionoff limitsAnonymized copybehaves like production
Three doors. Only one of them was usable.

Row by row: 20 hours

The previous solution fetched records in batches, changed them and sent them back as separate updates. That's correct, and at hundreds of millions of records it's very slow, because every row makes a round trip just to change a few fields. It took 20 hours.

one round trip per rowApplicationDatabase20 hours
Correct, and very slow.

Two kinds of data

Does the data have to leave the database at all?

That question was the starting point, and it split the data into two categories.

GeolocationSSNEmailFirst nameAddressMySQL functionsin the queryfakes tablea million records
Each column goes where it belongs.
MySQL functions can generate it in the query
  • Geolocation
  • Social security numbers
  • Emails
  • Phone numbers
  • Credit card numbers
SQL can't believably invent it
  • First names
  • Last names
  • Addresses

For the first group, the craft is in how the functions are combined. An SSN comes out looking like an SSN, an email like an email. For the second, we prepared a fakes table with a million records and one column per data type: first_name, last_name, street and so on.

Blanking every column would be easy, and would leave a useless database. So each column is treated according to what it holds.

One query, not a million round trips

A run starts by importing the fakes table, which takes very little time. The system then runs UPDATE queries according to its configuration. Each one either replaces values with functions right in the database or does a JOIN to fakes. No row travels to the application and back, so the database does all the work.

AppUPDATEno rows travel backdatabasefakescustomers
One query goes in. Nothing comes back but the result.

Still the same person

If Anna becomes Maria in one table, she's Maria in all of them. The data stays consistent across tables after anonymization, so relationships between records survive even though the people don't.

customersordersAnnaMariaJohnPaulAnnaMariaJohnPaul
The name changes everywhere at once, so the records still match.

Fast enough to be routine

20 hours Now: 10 minutes

Ten of the tables hold 20 to 30 million rows each. It covers 80 columns across 140 tables, finishes 100 GB in 10 minutes instead of 20 hours, and runs three times a week with a 99.98% success rate. A fresh, safe copy stopped being a day-long project and became something that's simply there. Something this sensitive has to be boring, and it is.

Before: 20 hoursNow: 10 minutes
Both bars are drawn to the same scale.

Case study

Access for every freelancer, and nothing more

We built a dedicated system for an advertising agency to manage its freelancers' work: accounts, repositories, environments and databases in one place. Each contractor got a full set of tools to work with, and access only to what they needed. It has been running since 2020 and is still in use.

  • 60+ freelancers
  • 200+ repositories, and still growing
  • 15 min for a new person to get started
  • 6 years in use and still running

More than a place for the code

The agency built mostly WordPress and Laravel solutions for its clients, and relied on a large, steady pool of freelancers. Keeping the code somewhere wasn't enough. It needed what GitHub offers, joined up with three things GitHub doesn't: hosting development versions of the apps, showing progress to the client, and settling up with freelancers. No ready-made tool tied all of that together, so we built one.

What GitHub doesDev hostingClient progressSettling upOne system
Four jobs that had never lived in one tool.

One place for every kind of access

The system created SFTP, Git and SSH accounts itself, so nobody had to go from server to server doing it by hand. The same route worked in reverse: the end of a contract meant revoking access in one place. A new person was at work after 15 minutes.

It all ran on Linux. We wrote the panel in PHP, but it never touched the server directly: system commands were carried out by a separate program in Go. Each freelancer had their own system account, created with useradd. Over SFTP, they saw their own projects and nothing else.

PHP panelGo programLinux serverSFTP accountGit accountSSH accountcreatesrevokes
One place to grant it, and one place to take it back.

A gate of our own for Git

An ordinary server account says nothing about who may touch which repository, so we wrote our own authorization gate for Git over SSH. A forced command in sshd read SSH_ORIGINAL_COMMAND: git-upload-pack means read, git-receive-pack means write, and the gate checked the user's rights to that repository before letting the operation through. A freelancer could read one repository, push to another and have no way into the rest.

FreelancerGit gateRepositoryreadRepositorypushRepositoryno way ingit-upload-packgit-receive-packno rights
The rights are checked per repository, before Git does anything.

SSH straight into the container

Connecting over SSH, a freelancer didn't land on the host but in the project's container. The account gave no shell on the server: ForceCommand took over every session and passed it to a small Go program, run through sudo with a single allowed command. Membership of the docker group is effectively root, and nobody from outside should get that.

The project was chosen by name: ssh -t [email protected] acme-shop. The program checked in the database whether this person had access to this project, then ran docker exec -it in its container with the user's UID, so files changed over SSH and SFTP had the same owner.

FreelancerGo programContainerForceCommanddocker execchecks access firstno shell on the server
Straight into the project, without passing through the host.

Separate environment, separate database

Accounts belonged to people, and environments to projects. Each project ran in its own Docker container with its own MySQL or PostgreSQL database, or several if it needed them. The databases were for development only, so the data of the agency's clients never reached freelancers. A mistake stayed inside its project: it didn't break anyone else's work or reach anyone else's data.

With hundreds of environments on shared infrastructure, one runaway process couldn't be allowed to stop the rest. So every container had CPU and memory limits, set with --cpus and --memory in Docker, which means cgroups in the Linux kernel.

ProjectMySQLCPU, memoryunaffectedProjectPostgreSQLCPU, memoryat its limitProjectMySQLCPU, memoryunaffected
A runaway process stops at its own limit.

A preview at its own address

A client or account manager could look at the work without access to anything else. The system created a subdomain in Nginx that sent traffic to the project's Docker container.

ClientNginxOtherThis projectOtherown subdomain
One address, one project, and nothing else.

A panel, roles and a trail behind every login

The agency ran all of this from its own panel. It could see who was working on what, and it granted permissions through roles and groups rather than person by person. That's also where it settled up with freelancers. An access log showed who had access to what, and when. Environments and databases were backed up twice a week.

RolesGroupsaccess logwho had accessto whatand when
Access granted by role and written down.

Case study

The database forgets. Events don't.

The product worked, but nobody could say exactly how it was used: who did what, what happened next, whether the payment went through, how often things were cancelled. A copy of the production database in Snowflake didn't answer it either. We built an event pipeline between engineering and the data team on Kafka, with agreed contracts for every event, and made it part of how features get built.

  • 7 engineering teams publishing events
  • Every event under an Avro schema
  • 0 analytics-only data kept in production

A copy of production isn't history

An external event tool and a few charts gave a rough picture, and the data team synced the production database into Snowflake for the rest. It still didn't add up. A database holds the current state, not the story of how it got there. Records get updated, records get deleted, and relationships the data team needed were never stored in the first place. You can't ask a copy what happened last Tuesday if the original has already forgotten.

Database rowcreatedpaidcancelledonly the last state is leftWhat actually happenedcreatedpaidcancelled
The database keeps the ending. Events keep the story.

Not in the production database

The obvious fix was to keep more in production: history tables, extra columns, nothing ever deleted. We said no. Data the application doesn't need still makes every query slower, every migration riskier and every change harder to reason about. The production database should serve the product, not double as a warehouse.

History tablesExtra columnsNothing deletedProductiondatabasewe said no
Three reasonable ideas, and none of them got in.

What happened, as it happened

The application now says what happened, as events on Kafka: who did what, what it led to, how a payment ended, what got cancelled. The data team reads that stream into Snowflake and builds the bigger picture from facts rather than from snapshots. The database sync stayed alongside it, for the times someone really does need the current state.

AppKafkaDatabase syncSnowflakewhat happenedcurrent state
Facts on one line, current state on the other.

No lost events

An event that's published for a change that rolled back is a lie. A change that saves but never publishes its event is a gap. We used the outbox pattern: the event is written in the same transaction as the change, then published from there. Either both happen or neither does.

one transactionChangeEventKafkaboth savedrolled back: neither
One transaction, so the change and its event can't disagree.

Contracts, not guesswork

Every event has a contract agreed between engineering and the data team: what it means, why it exists, which fields it carries. The contracts live as Avro schemas in Confluent Schema Registry, so a producer can't quietly change the shape of an event that someone downstream depends on.

changeEngineeringData teamcontractwhat it meanswhy it existswhich fieldsno quiet changes
Agreed by both sides and checked in Confluent Schema Registry.

Events from day one

The hardest part wasn't Kafka.

It was making events part of building a feature, not something added later when somebody asks. A new feature is designed with the data team, so its events exist on release day, and a week later the question "is this working, and is it worth it?" has an answer based on data.

Designwith data teamReleaseevents existA week lateranswer from data
The events ship with the feature.

Case study

Everyone could see everything. Now they can't.

Around $20 billion a year passed through one platform, along with social security numbers, driving licences, bank accounts and what every user earned. Nearly every staff account could see and change almost all of it. We designed a new roles and permissions system from scratch, rolled it out across the company and handed it over to the team that runs it now.

  • $20B a year through the platform
  • 100+ accounts, staff and outside partners
  • 8+ departments
  • ~30 roles
  • 3 levels of access to sensitive data

What was exposed

The system held social security numbers, certificates, driving licences, home addresses, bank accounts and invoices. It showed what each user earned, and with enough of those numbers you could work out what the company itself earned. Very few people needed that data, but almost everyone could open it.

Social security numbersDriving licencesHome addressesBank accountsInvoicesEarnings
Almost everyone, connected to almost everything.

Who had access

More than a hundred accounts: employees from engineering, product, design, analytics, sales, marketing, operations and support, plus outside companies handling support and operations as if they were staff. A couple of broad roles covered all of them, and between them they gave read and write access to almost everything. In practice, there was no access control worth the name.

EngineeringProductDesignAnalyticsSalesMarketingOperationsSupportPartnersBroad roleBroad roleAlmosteverything
Every department, through the same couple of doors.

Start with the work, not the roles

Roles drawn up in a meeting room end up either too loose or in everyone's way. We started by mapping how the company actually worked: which departments, which kinds of employee, which tasks they did in the system, and what data each task really needed. Around thirty roles came out of that map.

The goal had two halves, and both mattered. Nobody should lose the ability to do their job. Everybody should lose access they didn't need for it.

Departmentswhich onesKinds of employeewho does whatTaskswhat they do in the systemData each task needsand only thatAround thirty roleswhat came out of the map
The roles came last, not first.

Seeing a page isn't seeing the data

Opening a module is one decision. What you see once you're inside is another. Each role carries its own level of access to sensitive data, so two people on the same screen can see very different amounts of it, each exactly what their work calls for.

Support can see what it needs to help a user, while product and engineering, who work on the same screens, don't see the sensitive fields at all. Some people lost access entirely, because their work never needed it.

SupportUserSensitive fieldSensitive fieldProduct and engineeringUserSensitive fieldhiddenSensitive fieldhidden
Same screen, different amount of data.

Exceptions, with sign-off

Not every case fits a role. When someone needed more, they got a grant for that one person, either permanent or for a set time, and only after their supervisor and the security team had both approved it. Every grant was recorded in an audit log, so the answer to "who can see this, and why" is a lookup, not an investigation.

RequestSupervisorSecurityGrantpermanent or for a set timeAudit log
Two approvals before the grant, and a record after it.

Tight first, loosen on request

We rolled it out in stages, tested each one, then switched the company over. Where we weren't sure someone needed a permission, we left it out and waited for them to ask. Asking takes a minute and an approval. Undoing a leak doesn't work at all.

left out until someone asks
Permissions get added on request, one at a time.

Coordination was the work

The permission checks were the easy part.

Deciding what counts as sensitive, and how sensitive, was the company's call. We worked alongside its security and data governance teams to map the data in the system: they classified it, and we built that classification into the access levels. The rest was agreeing with every department what it needed, getting supervisors behind the approval process, and fitting the model to how this company worked rather than to a textbook. We designed it, rolled it out end to end and handed it over for maintenance.

Security teamclassifies dataAccess levelswe build it inDepartmentsagree on needsthen handed over for maintenance
Their classification, built into our system, agreed with every department.

Don't take our word for it.

Klusek is our own product, and it's in production. Open it and judge the work for yourself.

Your whole home, in one calm app.

Klusek splits bills, looks after plants and keeps shopping lists, chores and notes shared with the people you live with. We designed it, built it and run it.

A Klusek shopping list, grouped by category, with who added each item.
Klusek notes, with a pinned checklist shared by the family.
Klusek shared expenses, showing what you are owed and this week's bills.

New modules, all the time.

Any list we printed here would be out of date soon. Every module is free for yourself, forever. Sharing comes with Klusek Pass.

All of a household's Klusek lists, each with its own illustration.
A shared Klusek cookbook of recipes.
A Klusek plant collection, grouped by room, with each plant's care status.
A Klusek guest list for a birthday party, with confirmations.
The Klusek dashboard, summarising to-dos, plants, expenses, shopping and guests.
A Klusek to-do list, with today's tasks.
A Klusek project that groups several tools for one birthday.

What's underneath.

The stack behind it, for the technically minded.

  • APILaravel and PHPA REST API, with its client generated from the spec.
  • AppVue, TypeScript, CapacitorOne codebase for the browser and Android.
  • DataPostgreSQL and RedisStorage, cache, sessions and queues.
  • JobsLaravel HorizonBackground work, off the request path.
Klusek, the app's calico cat mascot, using a smartphone.

Live.

Not a mockup and not a slide. The demo opens a sample household with no account, and the cat comes with it.

  • AndroidOn Google Play.
  • Any browserNothing to install.
  • iPhone and iPadThe app is on its way.
  • Plain pricingNo ads. Free for yourself.

From first call to launch.

Five steps, the same every time, so you always know what happens next.

  1. Call

    Day 1, free

    Thirty minutes. You explain the problem, we ask the awkward questions early.

  2. Plan

    Week 1

    Scope, milestones and price in writing. Nothing starts until you agree.

  3. Build

    Weeks 2+

    Short cycles and a preview link every week, so you steer as we go.

  4. Launch

    Release day

    Monitoring, backups and a rollback plan in place before going live.

  5. Run

    Ongoing

    Updates, fixes and new features, or a clean handover to your team.

Two ways to work with us.

Pick the one that fits how clear your plans are. Switch later if they change.

A project

For a website, app or MVP with a clear goal and a date.

  • Fixed scope and fixed price
  • Paid per milestone
  • Launch, then hand over or keep it running
  • Technical audits work the same way
Usually 2 to 20 weeksPlan a project

Ongoing

For products that keep growing, or a team that needs a hand.

  • We join your tools and your rituals
  • Billed monthly, part-time or full-time
  • Adjust or stop with a month's notice
  • Invoices in PLN, EUR, USD or GBP
As long as you needTalk about it

Questions.

Anything else? Ask us. We reply within one business day.

Who owns the code?

You do, from the first day. It lives in your repository and your cloud account, so you're never locked in.

What language and time zone do you work in?

English or Polish, for calls, documents and code. We're based in Poland and work with Europe, the UK and the US.

Will you sign an NDA?

Yes, yours or ours, before the first call.

How do you price?

After a free call and a short planning phase you get a written estimate with scope, timeline and price. Nothing starts until you agree to it.

How soon can you start?

Usually within one to two weeks of agreeing the plan. Urgent fixes to a live system, often within days.

Can you take over an existing project?

Yes. We start with a short audit, tell you plainly what state it's in, and propose a plan: fix, improve or, rarely, rebuild.

I'm not technical. Is that a problem?

Not at all. We explain decisions in plain language, give you options with their trade-offs, and handle everything technical, from hosting to app store releases.

What do you build with?

Whatever fits the job and your team: Laravel, Node.js, Next.js, Go, Capacitor for iOS and Android, PostgreSQL, AWS and Docker.

How do you invoice?

In PLN, EUR, USD or GBP, monthly or per milestone.

Let's build it.

Tell us what you're working on. You'll hear back within one business day, usually with questions and a time for a call.

[email protected]

nocake is run by Tomasz and Piotr Żełudziewicz in Łódź, Poland.

© 2026 nocake, Łódź, Poland[email protected]