Refueler/Editorial

Editorial/Engineering · Trust

Engineering Trust Build notes

Looks Done Isn't Done

Two pieces of infrastructure — HMAC webhook verification and a PKCE authentication chain — pass every test I've written. Neither has been proven against reality. This is the honest answer to why the app isn't available yet.

June 2026 5 min read refueler.io/editorial/looks-done-isnt-done

There's a particular feeling familiar to anyone who has shipped software: the moment something looks finished. The endpoint responds. The tests pass. Everything suggests it's ready.

Sometimes that's true. Sometimes you've only proved your own assumptions.

The payment integration is built. The privacy layer is built — your phone knows where you are; Refueler doesn't. The order routing is built. What isn't built, in the sense of having been proven against something other than my own expectations, are the two components described below. They are the remaining blockers before the first user touches this.

A handshake that hasn't shaken

Part of how Refueler stays out of the payment layer is a webhook verification step. When a payment provider sends a notification — order settled, payment failed, retry needed — that notification arrives as an HTTP request signed with an HMAC derived from a shared secret. My implementation reproduces the same digest locally and compares. If they match, the request is genuine. If they don't, it's discarded.

The check exists. It's been in the codebase for a while now, passing every test I've thrown at it — and every one of those tests was written by me, using payloads I constructed and signed myself. Which is a bit like marking your own homework and being delighted to discover you got full marks. What it has not done is verify a single notification that actually arrived from the provider's infrastructure, signed by their system, with their canonicalisation of the request body and their timing on the timestamp. Until it does, I don't know whether my digest matches theirs, whether my replay window is correctly sized, or whether the implementation survives a production retry sequence.

HMAC webhook verification · Flow diagram · Open issue
How the check works — and where the gap is
Expected flow
PAYMENT PROVIDER

        │  POST /webhook

  Receive request


Recreate HMAC digest


Compare signatures
       / \
   Match  Reject

  Process
Today vs. still missing
  • Payload generated by me
  • Signature generated by me
  • Verification logic written
  • Unit tests passing
  • Live provider payload
  • Live provider signature
  • Provider canonicalisation confirmed
  • Replay attacks tested
  • Production retries under jitter
Flow is illustrative — architecture accurate, labels simplified.

The same lesson, a different track

The second piece is a PKCE authentication chain — the mechanism that gets a commuter from "I want to log in" to "I am logged in" on their phone, when the login itself happens in a browser rather than inside the app. PKCE was chosen specifically because it doesn't require a client secret stored on the device, which matters when the device can be lost, jailbroken, or inspected. The right choice on paper. The right choice in the simulator.

On a physical device, the OAuth callback occasionally arrives before the PKCE verifier can be recovered from Keychain. Whether that's a race condition between the browser returning control and the Keychain write completing, a persistence issue specific to how the item was stored, or something else entirely is exactly what still needs investigation. What surfaces is a login that appears to complete, and then doesn't. The simulator never shows this. Physical hardware does.

PKCE authentication chain · Flow diagram · Open issue
Where the login flow breaks on a physical device
Authentication flow
       PHONE


Generate verifier


 Store in Keychain


  Open browser


  User logs in


 OAuth callback


 Read verifier  ← ✗ Failure observed here


 Exchange code


  Logged in
Simulator vs. physical device
  • Verifier generated correctly
  • Keychain write initiated
  • Browser opens, user logs in
  • OAuth callback received
  • Simulator: full flow completes
  • Physical device: verifier readable
  • Race condition ruled out
  • Survives app backgrounding
  • Consistent across clean installs
  • Token refresh after expiry
Flow shows the PKCE exchange sequence. Failure point is accurately placed.

What connects these two isn't the engineering. It's the instinct. Both pieces produced output that looked correct. Both passed every check I'd thought to write.

What done actually means

Acceptance criteria · Both tracks · June 2026
How each component earns the right to be called finished
HMAC webhook verification Open
Looks done ✓  ·  Tests written  ·  Simulator: passing
Actually done ✗  ·  Live payload: never tested  ·  Replay: never tested
  • Live provider payload verified end-to-end against production secret
  • Malformed and replayed requests correctly rejected under load
  • Implementation survives provider retry sequences with timing jitter
  • Vault secret confirmed round-trip from storage through signature comparison
PKCE authentication chain Open
Looks done ✓  ·  Simulator: passing  ·  Flow: complete
Actually done ✗  ·  Physical device: fails  ·  Cause: under investigation
  • Physical device completes full exchange consistently across clean installs
  • Keychain timing issue identified and resolved
  • Login survives app backgrounding and interruption mid-flow
  • Token refresh confirmed working after session expiry on device
Criteria are minimum bars, not exhaustive. Updated when status changes.

Neither of these is a months-long rebuild. The webhook work closes once I have a sandbox endpoint receiving live provider notifications — that's the next concrete step. The Keychain issue is most likely a timing problem that the simulator papers over, which makes it diagnosable once I have the right device instrumented properly.

I could simply have delayed the launch and said nothing. I'd rather explain the specific engineering reasons than ask anyone to take "coming soon" on trust.

Refueler is not a payment processor. It doesn't hold your money. Your phone knows where you are; Refueler doesn't. Most of what the app asks you to trust is timing, not custody. But timing infrastructure still touches the moment a payment is confirmed and the moment your identity is verified, and you should know exactly how seriously the gap between "looks done" and "is done" is being taken before you ever have reason to test it yourself.

Good engineering isn't the absence of bugs. It's resisting the urge to call something finished before reality has had its turn — and then publishing the gap so that when it closes, the closure means something.

Notes

HMAC webhook verification · Full context · Expected flow vs. untested gaps
Payment notification verification — complete flow and open questions
Expected production flow

A payment notification arrives from the provider's server. My implementation reads the raw request body, recreates the HMAC digest using the shared secret held in a production vault, and compares it against the signature supplied in the request header. Match = process. No match = discard silently.

PAYMENT PROVIDER SERVER

         │  HTTP POST
         │  Body: raw JSON event
         │  Header: HMAC signature
         │  Header: timestamp

   MY WEBHOOK HANDLER

         ├─ Read raw body (pre-parse)
         ├─ Read timestamp header
         ├─ Check replay window
         ├─ Recreate HMAC digest
         │  (body + timestamp + secret)
         ├─ Compare to header signature

   Match → process event
   No match → discard silently
What hasn't been tested

Every test I've run constructs both the payload and the signature. The function has never received a notification it didn't help create. Until a live provider payload arrives, the following remain unknown.

? Provider's body serialisation order
  Does their JSON match the field order
  my digest function expects?

? Timestamp header field name
  Am I reading the right header?
  Is the format what I'm parsing?

? Replay window sizing
  Is ±5 minutes correct for this
  provider's retry cadence?

? Production vault secret round-trip
  Secret has never been used in a live
  verification. Only test values.

? Behaviour under retries
  Provider retries failed deliveries.
  Replay detection must handle this
  without rejecting legitimate retries.

Status: open — no fix landed
Next step: sandbox live payload test
Illustrative — shape and category of the gap, not the production implementation. No library names, actual header values, or secrets are disclosed. Source: internal engineering tracking, June 2026.
PKCE authentication chain · Full context · Simulator vs. physical device
Login flow — where simulator and physical device diverge
Simulator — passes every time

In the iOS Simulator, the full PKCE exchange completes without issue on every run. The verifier is written to Keychain, the browser opens, the user authenticates, the callback fires, the verifier is read back, the code is exchanged for a token, the session is established.

       APP OPENS

  ✓  Generate PKCE verifier

  ✓  Write verifier to Keychain

  ✓  Open browser (ASWebAuthSession)

  ✓  User authenticates

  ✓  OAuth callback received

  ✓  Read verifier from Keychain

  ✓  Exchange code for token

  ✓  Session established
Physical device — failure observed

On physical hardware in early testing, the flow breaks at the Keychain read. The callback arrives, but the verifier is not recoverable. The login appears to have completed — the browser closes, the app resumes — but the token exchange never happens. The session does not exist.

       APP OPENS

  ✓  Generate PKCE verifier

  ✓  Write verifier to Keychain
     (write initiated — success assumed)

  ✓  Open browser

  ✓  User authenticates

  ✓  OAuth callback received

  ✗  Read verifier from Keychain
     → item not found

  ✗  Exchange aborted

  ✗  Login appeared to complete.
     Did not.

Open question: race condition between
callback and Keychain write, or
persistence issue, or something else.
Under investigation on physical device.
Flow represents the observed behaviour in early physical device testing. Failure point accurately placed. Cause not yet confirmed — investigation ongoing. Source: internal engineering tracking, June 2026.
Acceptance criteria · Both tracks · Full definition of done
What "done" means for each open component — in full
HMAC webhook verification

This track closes when a live provider payload has been verified end-to-end under real conditions — not a payload I constructed, not a test secret, not a simulated retry. The full acceptance criteria:

Looks done ✓
  Unit tests: passing
  Logic reviewed
  Shared secret: in vault

Actually done ✗  — requires:

□  Live provider payload received and
   verified against production secret

□  Malformed requests correctly rejected
   (wrong secret, wrong body, tampered)

□  Replayed requests rejected
   (timestamp outside window)

□  Implementation survives retries
   with realistic timing jitter from
   provider's retry schedule

□  Vault secret verified round-trip
   from storage through comparison
   on a live notification

Next step: sandbox live payload test
PKCE authentication chain

This track closes when the full login exchange completes reliably on physical hardware across realistic conditions — not just the clean simulator path. The full acceptance criteria:

Looks done ✓
  Simulator: full flow passing
  PKCE verifier: generates correctly
  OAuth callback: received correctly

Actually done ✗  — requires:

□  Physical device: full exchange
   completes consistently on
   clean installs

□  Keychain timing issue resolved
   (race condition or persistence
   root cause confirmed and fixed)

□  Login survives app backgrounding
   mid-flow

□  Interrupted flows handled cleanly
   (no ghost sessions, no stuck state)

□  Token refresh confirmed on device
   after session expiry

Next step: physical device instrumentation
Criteria are minimum bars, not exhaustive test suites. Updated when the status of either track changes. Source: internal engineering tracking, June 2026.