A journal entry, written between pushes.
On November 9 I posted what I knew: the ecosystem was moving unevenly, and I was tired. Three days later, November 12 came around and the whole sitting lasted longer than I expected. The machine got hot. The logs were everywhere. Two packages released, two release notes broke, one bug made PHP cry TypeError on production-shaped data, and I went to bed with the CI green — finally.
I'm writing this the morning after, when the timestamps still mean something and the shame of the previous night is fresh enough to be useful.
Nov 12, late afternoon — the rewrite matures
The thing I've been carrying since v2.0.0 finally stopped being a workaround and became a finish line.
turso-api-client-php v2.0.0 is out, and the v1 API is gone. Just gone. No compatibility shim, no polite migration path from the old new Client('org', 'token'). The builder only entry point (Client::builder()->withToken(…)->withOrganization(…)) forced every consumer to decide what organization they were talking to — no implicit guessing from token. 53 of 53 operations from the pinned OpenAPI snapshot, SHA-256 328d56b8…, mapped into the coverage manifest. Typed exception hierarchy rooted at PlatformException. PSR-18/17/7 everywhere.
I spent most of this sitting writing about it in tests, docs, and a wiki that must not lie. That's the part nobody tells you: the rewrite was mostly verification. Making sure the pinned snapshot and the code agreed with each other — operation by operation — and then making sure the CI pipeline would enforce it forever after.
Nothing about this felt like progress. It felt like cleanup. But I have learned, the hard way, that cleanup at this layer is where the security and reliability actually live. Typed exceptions don't fix your app, but they do arguably stop you from catching a empty success body as if it were valid JSON.
Also, the upload-proof is real this time: a Docker harness that builds a clean PHP app (8.3 / 8.4 / 8.5) path-requiring the checkout, creates a scratch database with seed.type = 'database_upload', mints a JWT, uploads an SQLite dump with the correct PRAGMAs, reads the row back over Hrana /v2/pipeline, then — crucially — deletes the scratch database. All 8/8 checks green across three matrix legs, verified live against Turso Cloud on 2026-11-12.
I stopped believing "it compiles." I now believe "the scratch database was deleted."
Nov 12, evening — the envelope that lied
Here is the paragraph where I confess something.
DELETE /v1/organizations/{organizationSlug}/databases/{databaseName} replies with a scalar-string envelope:
{ "database": "my-app-production" } The snapshot said that clearly. My implementation pretended it didn't. Databases::delete() declared : array and returned $decoded['database'] ?? $decoded.
For a short window of v2.0.0, every delete operation on a database — the most basic database administration — threw a TypeError and nothing else. In the Docker upload proof, everything passed except scratch_db_deleted. I stared at the log for about a second before remembering that “assuming the contract is right” is exactly the kind of assumption this rewrite is supposed to kill.
The fix took five minutes. The context took three months.
Databases::delete() now returns string|array, returning the scalar when present, verbatim otherwise. The test asserts {"database": "db1"} as a contract-correct shape instead of the objectified version I had been shipping. v2.1.0 followed later in the day with the fix, the hermetic temp-path tests, PHPStan level 6, and the green matrix across 8.3/8.4/8.5.
Nov 12, night — the Laravel adapter hour
Between v2.0.0 dropping lint and v2.1.0 marking green, the Laravel adapter — turso-platform-laravel — shipped too. It is not a big package: a service provider, a facade, a publishable config (turso-platform-config), and a thin manager around the SDK client. It binds lazily, aliasing the binding to Client::class, so controllers and jobs can type-hint the core SDK directly.
It shipped because it was ready, not because a blog post needed a new number. v1.0.0 went to Packagist 782 releases into the ecosystem. CI Matrix PHP 8.3/8.4/8.5 × Laravel 12 + package-discovery tests (7 tests / 14 assertions) all green, submitted manually because Packagist doesn't auto-bootstrap unprepared GitHub repos the same way.
And then PHPStan level 6 walked in and ruined something I had been very pleased with.
TursoPlatformManager::token() did:
$token = getenv('TURSO_PLATFORM_API_TOKEN') ?? ''; if ($token === '') { throw new ConfigurationException('…'); } getenv() returns string|false. The ?? '' only catches null. So a fully unset env var produced false, false === '' was false, no exception was thrown, and false walked straight into withToken(). The documented “missing token → ConfigurationException” path never ran, anywhere, ever.
The fix is boring:
$token = getenv('TURSO_PLATFORM_API_TOKEN'); if (!is_string($token) || $token === '') { … } PHPStan had found this before any of us had: it was the one error level 6 reported after we cleaned the ~50 iterableValue docblocks — and it was a real bug, not style. v1.1.0 followed hours later.
If you remember one thing from this, remember this sentence: static analysis that only catches style is a lint, static analysis that catches wrong code is a witness.
The Oh-No Moments Nobody Publishes
Three failures from this sitting that are not in most changelogs, but are in every honest one. They happened in this order:
First, .github/workflows/tests.yml got rejected on push. My token didn't carry the workflow scope. gh auth refresh -h github.com -s workflow — more specifically env -u GITHUB_TOKEN gh auth refresh -h github.com -s workflow because GITHUB_TOKEN was set in the shell and masking gh's keyring — fixed it. The workflow file itself was fine. The credential discipline was not.
Second, the CI was red on all three legs. Not because of the code. Because tests/IntegrationsTest.php wrote its SQLite scratch files to /tmp/opencode/pu_test_xxx.db, which is a path that exists on one developer workstation in this entire universe. The runner cannot see it. SQLSTATE[HY000] [14] unable to open database file, and it failed 78 tests in three matrix legs at once. The fix was un-glamorous: sys_get_temp_dir() + bin2hex(random_bytes(4)). Two lines. A whole CI afternoon.
Third, the release notes were blank. Both v2.1.0 and v1.1.0 initially went out with the body text File not found:. The notes file had been created by a shell that glued together a multi-line heredoc incorrectly and secreted the literal string “File not found: ” into the markdown file. GitHub then rendered exactly that. You were the one who told me — “Release note hanya menampilkan File not found:.” Both bodies were rewritten within the hour, the family was scanned (all other releases were fine), and the lesson — verify the release body before announcing, not after — went into the global checklist.
I will keep this pattern on screen forever, as the fastest lesson to teach the next person who encounters my work: the hardest bugs are paths, the hardest errors are blanks, and the toughest days are the ones where nothing looks broken until you look at it sideways.
The Weekend Checklist
Between everything, the docs caught up. The PHP ecosystem now has:
- An at-a-glance table in the ecosystem repo listing every package, version, and requirement — verified against Packagist and tags, not memory.
- A map of 53/53 operations to their SDK resource classes (
docs/api-map.mdfor the SDK,docs/facade-map.mdfor the Laravel adapter), with the live contract quirks documented — scalar-string delete envelope, empty-2xx bodies, v3 UUID-vs-slug, upload pre-flight PRAGMAs. - A troubleshooting doc for every failure we observed. No invented symptoms. No “might happen.”
- Wikis mirroring all of it (repo docs remain the source of truth — wikis are projections, not originals).
- A banner / art / README refresh for both package siblings — because ecosystems where your eyes land matter.
On November 13 the ecosystem README got its own correction: the Laravel and Symfony "Examples" sections were removed, per my own instinct that the example apps have earned their keep but not the cover page. They still exist; they are now merely not advertised.
Current Status
-
turso-api-client-phpv2.1.0 — released, Packagist^2.1, CI 3/3 green, 53/53 operations, upload-proof verified. -
turso-platform-laravelv1.1.0 — released the same sitting, Packagist crawled via webhook (manualupdate-packagerejected my.envtoken; the webhook wins every single time — always verify via thep2metadata instead of assuming an HTTP request says either yes or no). -
turso-client-httpv3.0.1 shipped earlier in the week (verified green) — the HTTP layer of the family of "A thing youcomposer requireand then forget exists." -
turso-installer's README now carries correct requirements, PHP 8.1–8.5 extension coverage; its queue is honest about what's outstanding. - The ecosystem README posted today — Nov 13, 2026.
These are not ordinary numbers for someone doing this between client projects. None of this was planned to happen inside one sitting. That's the grace — and the pathology — of a solo-maintained ecosystem.
The Takeaway
I woke up November 13 with two virtual pages of what I had done and an eight-word summary of how it felt:
"It's not done until CI says so."
It wasn't a rule before the sitting. It is a rule now — one release, one badge, one verification at a time.
The AI didn't do the work. The AI helped with parts of the mechanism, but the judgment — what “done” means, when to stop designing versus start releasing, which env file contains a token that will be rotten tomorrow — none of that got delegated. I wrote every claim in the docs by verifying it, checking it off against a repo tag or a CI log. It took longer than "paint the API and ship it." It was also the only way I knew I could keep the library I was asked to maintain.
It is the second time I have said this somewhere public, and I still mean it: trust evidence, not résumés. Trust the scratch database being deleted and the next release shipping clean. Everything else follows.
The numbered list for the people who skimmed:
-
turso-api-client-phpv2.0.0 (2026-11-12) — typed rewrite against the pinned OpenAPI snapshot, 53/53 operations, builder-only, org-scope explicit. -
turso-api-client-phpv2.1.0 (2026-11-12) — DatabaseUploader / RegionResolver integrations, scalar-string delete envelope fix, PHPStan level 6, hermetic CI 8.3/8.4/8.5, upload-proof matrix green live. -
turso-platform-laravelv1.0.0 (2026-11-12) — SP + Facade + publishable config, 7/14 Testbench suite green. -
turso-platform-laravelv1.1.0 (2026-11-12) — PHPStan level 6 + thegetenv()bugfix, docs & wiki & art. -
turso-client-httpv3.0.0 → v3.0.1 — v3 line live. - Repo of record —
turso-php-ecosystemREADME refreshed (2026-11-13).
Previous post in this series: Maintaining the Turso PHP Ecosystem in My Spare Time: What Happened on November 9, 2026.
License: CC-BY-SA. Code in the linked repositories; errors live here because I wrote them.


