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 device with a companion app. Over several years I owned the embedded firmware, the wireless protocol it speaks, the codec library that decodes it, the cross-platform app that drives it, and the desktop tool engineers use to inspect and flash units in the field.

The decision that mattered came early and looked like extra work at the time. The original design sent character commands over the wire, composed wherever they happened to be needed. I replaced that with an actual protocol — a byte-level encoding with framing, a control byte carrying type and command, and a length-prefixed payload — and then wrote it down as a specification rather than leaving it implicit in whichever codebase happened to speak it last.

That is the whole engagement in one choice. Because the protocol was a document and a codec library rather than a habit, the client can commission hardware from someone else and hand them the spec. New devices do not require a new app. And when the wireless transport later moved from Bluetooth Classic to BLE — because Classic needs Apple's accessory program on iOS and BLE does not — the codec needed no change at all. Three of the five layers moved. The one describing what the bytes mean sat still, because it had never been coupled to how they travelled.

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 recognise 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.
  • 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.