<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alexandre Gertsch</title>
    <description>The latest articles on DEV Community by Alexandre Gertsch (@alexandre_gertsch_f02622a).</description>
    <link>https://dev.to/alexandre_gertsch_f02622a</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4180287%2Fe27df99c-a9cc-4e68-9230-4a210001333c.png</url>
      <title>DEV Community: Alexandre Gertsch</title>
      <link>https://dev.to/alexandre_gertsch_f02622a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexandre_gertsch_f02622a"/>
    <language>en</language>
    <item>
      <title>Building a pay-per-use API platform on shared PHP hosting: no Node, no Docker, no queue</title>
      <dc:creator>Alexandre Gertsch</dc:creator>
      <pubDate>Mon, 12 Oct 2026 20:24:14 +0000</pubDate>
      <link>https://dev.to/alexandre_gertsch_f02622a/building-a-pay-per-use-api-platform-on-shared-php-hosting-no-node-no-docker-no-queue-4p5</link>
      <guid>https://dev.to/alexandre_gertsch_f02622a/building-a-pay-per-use-api-platform-on-shared-php-hosting-no-node-no-docker-no-queue-4p5</guid>
      <description>&lt;p&gt;I'm a solo developer in Geneva and I just built &lt;strong&gt;Supercalcula API&lt;/strong&gt;, a small platform of utility APIs sold by usage: QR codes and barcodes, email verification, CSV/JSON/XML/YAML conversion, SEO analysis, PII redaction, and currency/unit conversion.&lt;/p&gt;

&lt;p&gt;The interesting constraint: it runs on &lt;strong&gt;ordinary shared hosting&lt;/strong&gt; (Infomaniak). No Docker, no Redis, no queue, no Node on the server. Here is what that taught me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PHP 8.4, Slim 4, Twig, PHP-DI, PDO + MariaDB&lt;/li&gt;
&lt;li&gt;Stripe Checkout, Customer Portal and webhooks&lt;/li&gt;
&lt;li&gt;Tailwind compiled locally and committed, so the server never needs a build step&lt;/li&gt;
&lt;li&gt;PHPUnit with a real MariaDB test database, PHPStan level 6&lt;/li&gt;
&lt;li&gt;Three languages (FR/DE/EN) with translated URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Quota in credits, reserved then settled
&lt;/h2&gt;

&lt;p&gt;Each endpoint costs a number of credits. The gate reserves the cost before the handler runs and settles it afterwards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a successful call costs the endpoint's credits&lt;/li&gt;
&lt;li&gt;a client error (4xx) costs 1 credit, so validation can't be abused for free&lt;/li&gt;
&lt;li&gt;a server error (5xx), a bad key (401) or a rate limit (429) costs nothing&lt;/li&gt;
&lt;li&gt;monthly quota is consumed first, then purchased credit packs, oldest first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rate limiting is a sliding window stored in the database in 10-second buckets. It's not Redis-fast, but it's boring and it works on shared hosting.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. No cron? Make the cron call you
&lt;/h2&gt;

&lt;p&gt;The host's scheduler can only call a URL, once per hour at best. So every background job (daily cleanup, Stripe reconciliation, exchange rates, status checks, encrypted backups) is a protected URL secured by a long random token, and the scheduler just hits it. Locally the same tasks run as CLI scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Stripe webhooks must be idempotent
&lt;/h2&gt;

&lt;p&gt;Every event is stored by its id before being processed, so a retry never double-credits a customer. I also added a reconcile job that compares Stripe with my database, because webhooks do get missed (I had one subscription event missing from my own endpoint configuration; the reconcile job caught it).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Privacy by construction
&lt;/h2&gt;

&lt;p&gt;The request log stores the endpoint, status, duration, credits and an IP hash with a daily salt. It never stores the data you send. I verified it: I sent 50 calls containing a marker string, then searched the whole production database and every file for it. Zero hits.&lt;/p&gt;

&lt;p&gt;One honest caveat: for GET endpoints the parameters live in the URL, and the hosting provider's access logs may keep full URLs. I documented that in the privacy policy rather than pretending otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Deploying without Node, Docker or SFTP
&lt;/h2&gt;

&lt;p&gt;My host's SFTP subsystem didn't work, so the deploy script uses FTPS. It runs the tests and static analysis first and refuses to deploy if either fails. Then it syncs files, switches on a maintenance flag, runs the database migrations behind a secret token and removes the flag. A rollback script restores the previous package.&lt;/p&gt;

&lt;p&gt;A small trap that cost me an hour: asset versioning (&lt;code&gt;app.css?v=hash&lt;/code&gt;) must hash files from the real document root in production, otherwise browsers keep serving the old CSS for a year.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd like your help
&lt;/h2&gt;

&lt;p&gt;The platform is in private beta, in &lt;strong&gt;Stripe test mode&lt;/strong&gt;: nothing is charged. Free accounts get 200 calls a month. I would love feedback from people who actually use APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the documentation clear? There's a "try it with your key" panel on every endpoint.&lt;/li&gt;
&lt;li&gt;Do the examples work as written?&lt;/li&gt;
&lt;li&gt;Anything confusing in sign-up, the dashboard or the billing flow?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;a href="https://supercalcula.ch/en/" rel="noopener noreferrer"&gt;https://supercalcula.ch/en/&lt;/a&gt; (also in French and German)&lt;/p&gt;

&lt;p&gt;You can test the subscription flow with Stripe's test card &lt;code&gt;4242 4242 4242 4242&lt;/code&gt;. Please don't send real or sensitive data. Comments here or &lt;a href="mailto:contact@supercalcula.ch"&gt;contact@supercalcula.ch&lt;/a&gt; are both welcome.Thanks&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built this with the help of AI coding tools (Claude Code), reviewing and testing the result myself.&lt;/em&gt; &lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>api</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
