Manila · --:--
Writing

Joseph Harvey Angeles

Manila
10+ years
Systems & architecture
Mostly after 22:00

I build systems that outlast the problems they solved.

I design and build systems across mobile, web, backend and firmware — usually the layer other teams hand off. I've spent the last decade doing it for clients who needed the whole thing, not a slice of it. I run Night Owl Software Studios, a small studio in Manila.

Currently

Taking two projects for 2026 · reading about protocol design · still convinced most software should be smaller.

Approach

How I work

The best systems balance elegance with practicality. Code should be maintainable and aligned with where a business is going, not just patched for where it is. Often the most valuable thing I do is talk a client out of building something.

I work with AI agents the way I'd work with a team — they draft, I set the constraints, and nothing ships without review. In practice that's meant an unusual amount of adversarial review: a second pass whose job is to break what the first one wrote. The leverage is real. The judgment is still the job.

Five engagements

Selected work

A bug could start in the UI and end in firmware.

A connected hardware platform

A consumer hardware line with a companion app. Over several years I owned the embedded firmware, the protocol it speaks, the codec library that encodes and decodes it, the cross-platform app that drives it, and the desktop tool engineers use to inspect and flash units in the field.

The earlier devices were passive — the app played audio and they did something physical with it. The newer one is active, closer to a media player with its own storage, and it needed to be talked to. A third-party vendor shipped its first firmware doing that by sending serialized JSON over Bluetooth Classic. It worked, and it was enormously wasteful.

I argued for a real protocol instead: a byte-level encoding with framing, a control byte carrying type and command, and a length-prefixed payload — written down as a specification rather than left implicit in whichever codebase spoke it last. That was a harder sell than it sounds, because the thing it replaced already worked.

It paid for itself twice. I later took over the firmware from the vendor and built on it, which is a great deal easier when the wire format is a document rather than a habit — and the same document is what lets the client commission hardware from someone else entirely. Then I moved the transport from Bluetooth Classic to BLE, which is what finally let iOS users in; Classic had effectively made it Android-only. The codec did not change. Consider what that migration would have looked like still shipping JSON, into BLE's packet sizes.

The rest was less elegant. The native audio player needed capabilities neither web view gave me, so I wrote a proper Capacitor plugin with iOS and Android implementations behind one interface rather than special-casing per platform in app code. A device-name field computed its length in UTF-16 code units while the payload was UTF-8, so accented names were silently truncated on the wire — and a 256-byte name wrapped to zero, which the protocol defines as leave unchanged, turning a rename into a convincing no-op. Auditing that turned up a use-after-free in the firmware's rename handler that had been there since the first commit.

The one I remember is a test jig that would not talk to one particular USB bridge chip. It was a wiring fault, and it was fixed with a soldering iron rather than a commit — which is why there is no trace of it in the history at all.

Three front ends, one tenant model.

A multi-tenant compliance platform

Three front ends over a single backend: the workspace where records and regulatory filings are managed, an internal control panel, and a self-service portal for the workforce it covers. I designed and built it.

The design decision was to make tenants data rather than code. Every tenant carries its own configuration, and an administrator changes what a tenant sees from the control panel — which fields are visible, which are editable, which features exist at all — and it appears in that tenant's UI without a deploy. The alternative is branching on tenant identity, and the alternative is how multi-tenant systems become unmaintainable: every new client adds a conditional somewhere nobody can find later.

The other hard part was authentication. Tenants arrived with whatever identity provider they already had — Okta, Azure, Keycloak — and we had our own token auth that everything internal depended on. Rather than pick one and force it, I extended the framework's auth layer so both could authenticate the same request pipeline. That meant reading the framework's source, not just its documentation, because the extension points I needed were not the ones the docs described.

It cost me later, in the way that clever things do. API-token requests were failing silently across the platform: our audit log wrote the acting user's id into a column with a foreign key to the users table, and a token's principal is synthetic — it has no row there. Every token-authenticated write hit a constraint violation and surfaced as a generic 500. It was invisible for as long as it was because nobody using a browser ever took that path.

The same investigation turned up something quieter. A reporting view joined each record to the user who created it — an inner join, which is correct until the creator is a token and the column is null. Null matches nothing, so those rows did not error; they simply were not there. The records existed. The report just did not mention them. That is the kind of defect that surfaces during an audit rather than before one, and it is the reason I left the critical paths behind 241 regression tests.

I also wrote the tooling that moved a client off their legacy system — 1,280 records, with dry-run and re-runnable imports. Idempotency was genuinely awkward: the source had no natural key and I could not add one to the destination, so already imported had to be inferred from the shape of the record itself.

Two major versions, one pair of hands.

A consumer social platform

A community app for a niche recreational audience: activity logging, feeds, and the social graph connecting them. I built the backend and data model, the cross-platform app, and the admin tooling, and carried it through two major versions.

The first version had a persistence layer I wrote myself, mapping query results to objects by position. It worked until two adjacent fields in one model were declared in a different order than they were read, and a comment's parent id started coming back as its parent. Nothing threw. The data was simply wrong in a way no test was looking for. That bug is most of the argument for the second version, which deleted the hand-rolled framework entirely and moved onto a maintained one — the data model survived unchanged; only the machinery around it went.

There is no follow table anywhere in the schema. Interest is derived: engage with a thread and you receive its notifications until you mute it. That was the right call for the product and it made the feed expensive — building a page meant fetching each post's author, attachments, likes and comment count separately, and the popularity sorts had to be pushed down into database functions to stay usable. I would describe that as moving the problem rather than solving it.

Two smaller things I think about. I deleted roughly eight and a half thousand lines of JavaScript image-cropping code and used the operating system's own photo editor instead. And I once shipped a backend change that hid posts until their photos finished uploading, before the app that understood it had shipped — so I turned it off again and re-enabled it a release later. Coordinating two release trains is not a technical problem and it will still cost you a day.

Sixty-eight diagrams. Someone had to draw them.

A high-value transaction platform

Roughly twenty services over an event bus, with signed inter-service requests and a four-stage release promotion pipeline. I was brought in for architecture documentation and targeted security work — which is to say, brought in to understand a system nobody had written down.

The documentation was the deliverable: a 61-page developer handbook with 68 architecture diagrams, each service page grounded in real file and line references so it could be checked rather than believed. Writing it is also how I found the interesting bug.

Order payment had three routes — wallet balance, card, or a split across both — and each carried its own idempotency guard, checking the ledger for its own kind of entry before charging. Every guard was correct. The bus guarantees at-least-once delivery, so a payment event can arrive twice; the first delivery spends part of the wallet balance and writes a split-payment entry. The redelivery then re-enters the same code with a different balance, and the dispatcher routes it down the card-only branch instead. That branch looks for a card-only entry, does not find one, and concludes this is a fresh payment.

It charges the card again. Not a race — a routing decision that silently changes on re-entry because shared state has already moved. It was hard to see precisely because each path was right in isolation; the defect lived in the seam between them, and no test that exercised one path could find it. The fix was to make every path recognize every path's entries.

The rest was hardening: an internal money-moving endpoint that sat behind a path prefix the rate limiter exempted, and request handling that would read an upstream response of any size into memory. Both are the same lesson in different clothes — the dangerous code is the code nobody thought was on the boundary.

Here the hard part was care, not features.

A recovery and mental health platform

Program content, progress tracking, and a community where members post and support one another. The feature list was not the difficult part.

Two decisions still look right to me. There is no display-name field in the schema at all — names shown in the community are derived, never stored, because a peer-support product should not accumulate an identity it does not need. And counsellors can only see the members assigned to them. That one is a constraint on staff rather than on users, and it exists so that assessment answers do not become browsable by anyone with a clinical login.

The mistake I remember was in bulk import. A change to how members were matched to their counsellor and group called the wrong lookup for one of the two, so people could be assigned to the wrong group quietly and correctly-looking. On most products that is a data bug. Here it is a care-routing failure — the wrong person receives someone's progress. We reverted it the same day rather than patching forward, and re-implemented it properly a few hours later.

I would not describe the moderation model as finished. Anything above a member role can remove a post, which is a blunt instrument, and I have thought since about what a real review queue would have looked like on a platform where the cost of leaving something up is not measured in complaints.

Project details withheld. Most of this work is under NDA, which tends to come with the kind of engagement where you're handed the whole system rather than a ticket.

Public code

Public work

The client projects are under NDA; this isn't. If you want to see how I actually write, start here.

  • Attesto — Receipt validation for App Store and Google Play purchases. Apple JWS chain verification against pinned roots, encrypted per-tenant credentials. MIT. It exists because an appeal failed: a hardware-linked app argued its purchases sat outside in-app purchase, Apple disagreed, and the validation layer we then had to build seemed worth owning properly rather than renting.
  • Babelon — AI-assisted localization that preserves ICU plurals and placeholders. Rust CLI for CI pipelines.
  • @nosslabs/iap — In-app purchases for Capacitor. 282 tests, 96% coverage; fixes upstreamed into the native plugin it builds on.
  • @nosslabs/bluetooth-classic — Bluetooth Classic for hybrid apps, native on Android and iOS behind one TypeScript contract.
Where I've been

Background

Speaker, Quasar Conference 2020 — building multi-platform applications.

2020 — now
Night Owl Software Studios — Founder & Lead Engineer
2018 — 2020
Freelance Full-Stack Engineer
2017 — 2018
Accenture — Application Developer, Team Lead
2014 — 2017
Nokia Networks — Software Specialist / R&D Engineer 3

Clients across the studio and freelance years include Ashnu Technologies, DigitalCandy, DLDNation, LJK Inc. and REWW.

Say hello

Contact

I take on a small number of projects a year where I can own the outcome end to end. If you have something you'd like built properly, I'd like to hear about it.

For studio engagements with a team behind them, that's Night Owl Software Studios. Everything on this page is work I take on personally.