<?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: Harry Agustiana</title>
    <description>The latest articles on DEV Community by Harry Agustiana (@harryagustiana).</description>
    <link>https://dev.to/harryagustiana</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%2F2761971%2Ffecc5812-e25b-43ff-81c4-398560a06443.jpeg</url>
      <title>DEV Community: Harry Agustiana</title>
      <link>https://dev.to/harryagustiana</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harryagustiana"/>
    <language>en</language>
    <item>
      <title>I Almost Shipped a Broken Database on Tencent EdgeOne Makers. Here's What Fixed It</title>
      <dc:creator>Harry Agustiana</dc:creator>
      <pubDate>Sat, 29 Aug 2026 06:25:27 +0000</pubDate>
      <link>https://dev.to/harryagustiana/i-almost-shipped-a-broken-database-on-tencent-edgeone-makers-heres-what-fixed-it-19h2</link>
      <guid>https://dev.to/harryagustiana/i-almost-shipped-a-broken-database-on-tencent-edgeone-makers-heres-what-fixed-it-19h2</guid>
      <description>&lt;p&gt;Here's a mistake I bet a lot of developers coming from traditional hosting will make on their first serverless edge project: assuming a local database file will just... work.&lt;/p&gt;

&lt;p&gt;I made that mistake building &lt;strong&gt;OptiQ&lt;/strong&gt;, a sales-tracking app for small optical shops, on &lt;strong&gt;Tencent EdgeOne Makers&lt;/strong&gt;. This post is less "here's a polished tutorial" and more "here's the debugging path I took," because I think the reasoning matters more than the final answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Project, Briefly
&lt;/h2&gt;

&lt;p&gt;Small optical shops have a messy sales pattern that most POS templates don't handle well. A customer might buy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Just a frame&lt;/li&gt;
&lt;li&gt;Just a lens&lt;/li&gt;
&lt;li&gt;A complete pair (frame + lens)&lt;/li&gt;
&lt;li&gt;Softlens&lt;/li&gt;
&lt;li&gt;Small accessories (cleaning solution, frame screws) — no prescription needed
On top of that, the shop needs to track refraction data (OD/OS, cylinder, axis) per customer, and &lt;em&gt;sometimes&lt;/em&gt; link it to a sale — but not always. OptiQ is my answer to that: a small, relational, low-cost app built specifically around that flexibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Low-cost was non-negotiable here. I'm the only IT person at my company, so I don't get to justify infrastructure spend for a side project — it has to run on free tiers, or it doesn't ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #1: Treating an Edge Function Like a Regular Server
&lt;/h2&gt;

&lt;p&gt;For context, EdgeOne Makers is the rebrand of EdgeOne Pages (as of June 2026) — it grew from "frontend hosting" into a full-stack platform for Web and AI Agent apps, with support for React, Vue, Next.js, Astro, and a few agent frameworks. That expanded scope is what made it worth using for something beyond a static site.&lt;/p&gt;

&lt;p&gt;My plan was simple: static frontend on Pages, API logic on Pages Functions, SQLite file for storage. Deploy was smooth — genuinely, push-to-live in minutes, no CDN or SSL setup needed.&lt;/p&gt;

&lt;p&gt;Then I hit the wall: &lt;strong&gt;edge functions are stateless.&lt;/strong&gt; There's no persistent filesystem, and a given request might get routed to a different edge instance than the last one. A &lt;code&gt;.db&lt;/code&gt; file written on one request could be gone — or inconsistent — by the next. This isn't an EdgeOne-specific limitation; it's true of serverless edge compute generally. But it's an easy thing to &lt;em&gt;forget&lt;/em&gt; if your mental model still comes from VPS-style deployments.&lt;/p&gt;

&lt;p&gt;Lesson: figure out your state strategy before you write a single API route, not after your first weird bug report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #2 (Almost): Picking a Database That Sleeps When You Need It Awake
&lt;/h2&gt;

&lt;p&gt;Once I accepted I needed a managed database, I evaluated three options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1 — EdgeOne's native KV store.&lt;/strong&gt; Fast, simple, and built in. But it's key-value only — no joins, no relational queries. For OptiQ, that would mean hand-rolling indexes for every access pattern (&lt;code&gt;transactions by customer&lt;/code&gt;, &lt;code&gt;transactions by date range&lt;/code&gt;) and computing monthly totals by pulling records into app code and summing manually. It works, but it's the kind of thing that quietly turns into technical debt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2 — Supabase.&lt;/strong&gt; Full Postgres, generous free tier, great DX. Then I read the fine print: free projects &lt;strong&gt;pause after ~7 days of inactivity&lt;/strong&gt; and need a manual restore to wake back up. A shop that doesn't sell something every single day could open the app to a database that's asleep. That's a bad failure mode for a business tool, even a small one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 3 — Turso.&lt;/strong&gt; SQLite (libSQL), but run as a managed service and queried over HTTP — which means it works fine from a stateless edge function, unlike a local file. Real SQL (joins, constraints, aggregates), no idle-pause behavior. This is what I went with, and the schema I'd already sketched needed almost zero changes to work with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part of the Schema That Actually Matters
&lt;/h2&gt;

&lt;p&gt;Most of OptiQ's schema is unremarkable — customers, products, transactions, transaction line items. The one decision worth calling out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;AUTOINCREMENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;refraction_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;refractions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;-- nullable, on purpose&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="nb"&gt;REAL&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;refraction_id&lt;/code&gt; is nullable. That's the whole trick. Without it, I'd have needed separate transaction tables (or awkward sentinel values) for "sale with a prescription attached" vs "sale without one" — like buying a bottle of lens cleaner shouldn't require a fake prescription record just to satisfy a NOT NULL constraint. One nullable foreign key instead of a schema fork.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where EdgeOne Makers' Full-Stack Positioning Actually Paid Off
&lt;/h2&gt;

&lt;p&gt;The thing I didn't expect going in: not having to stitch together three separate vendors. Static hosting, edge API functions, and (once I wired it up) the database connection all lived in one deploy pipeline. If you've ever debugged a CORS or env-var mismatch between a frontend host, a serverless function provider, and a database vendor that don't share tooling, you'll get why that's worth mentioning.&lt;/p&gt;

&lt;h2&gt;
  
  
  If You're About to Build Something Similar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Decide your state/database approach &lt;em&gt;before&lt;/em&gt; your first API route — not as a fix after deploying.&lt;/li&gt;
&lt;li&gt;Check what happens to your database on &lt;em&gt;inactivity&lt;/em&gt;, not just under load. "Free tier" often hides a failure mode in the fine print.&lt;/li&gt;
&lt;li&gt;Use nullable foreign keys deliberately when your domain has "sometimes this relationship exists, sometimes it doesn't" — it's cheaper than modeling every variant as a separate table.
OptiQ is live at &lt;a href="https://optiq.logota.biz.id/" rel="noopener noreferrer"&gt;optiq.logota.biz.id&lt;/a&gt; — built on Tencent EdgeOne Makers as part of Codepolitan's DevHandal 2026 Batch 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;#TencentEdgeOne #EdgeOneMakers #CODEPOLITAN #EdgeOne&lt;/p&gt;

</description>
      <category>tencentedgeone</category>
      <category>edgeonemakers</category>
      <category>codepolitan</category>
      <category>edgeone</category>
    </item>
    <item>
      <title>How a Broken Laravel App Turned Into Two Open Source Security Tools</title>
      <dc:creator>Harry Agustiana</dc:creator>
      <pubDate>Mon, 13 Jul 2026 05:11:32 +0000</pubDate>
      <link>https://dev.to/harryagustiana/how-a-broken-laravel-app-turned-into-two-open-source-security-tools-287c</link>
      <guid>https://dev.to/harryagustiana/how-a-broken-laravel-app-turned-into-two-open-source-security-tools-287c</guid>
      <description>&lt;p&gt;I now run a small self-hosted watchtower that pings me on Telegram the moment something looks wrong on any Laravel app I manage. It didn't start as a plan. It started with a friend's text message and a server that had already been compromised for who knows how long.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app nobody thought was a target
&lt;/h2&gt;

&lt;p&gt;The app was PaketLebaranku.id — a small Laravel + MySQL + Livewire platform running on a modest VPS through Biznet Gio, built for a friend's small business. Its only job: track installment savings for people paying into a Lebaran package program, small recurring deposits that add up to something meaningful once a year.&lt;/p&gt;

&lt;p&gt;It's exactly the kind of app that never shows up in anyone's threat model. No enterprise budget, no dedicated ops team, no security monitoring — just a business owner and a friend who built the thing on the side. Which, as it turned out, is precisely why it got hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  "The app is throwing a 500 error, can you check?"
&lt;/h2&gt;

&lt;p&gt;That was the whole message. I expected a stack trace, maybe a bad migration or an expired dependency. Instead, digging into the error turned up something far worse.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file wasn't misconfigured — it was gone. Any Laravel developer knows what that means: database credentials, app keys, third-party secrets, either already exposed or already pulled by whoever got in.&lt;/p&gt;

&lt;p&gt;Then a second find: a handful of backdoor scripts sitting inside &lt;code&gt;public/icons&lt;/code&gt;, dressed up to look like they belonged there. Then a third: &lt;code&gt;.htaccess&lt;/code&gt; had been rewritten to let Python scripts execute inside a directory that should never be able to run anything.&lt;/p&gt;

&lt;p&gt;Three separate footholds, sitting quietly in an app tied to real people's savings, for who knows how long before a user happened to hit an error page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap that actually mattered
&lt;/h2&gt;

&lt;p&gt;Once we'd contained it, the obvious question was "how do I catch this faster next time." But sitting with it longer, I realized the real failure wasn't a missing check — it was that nothing was watching at all. A scan run once, manually, after the fact doesn't help. The incident wasn't caught by a security tool; it was caught by luck, in the form of an annoyed user.&lt;/p&gt;

&lt;p&gt;That reframed what I actually needed to build: not just something that &lt;em&gt;could&lt;/em&gt; find this class of problem, but something that would look for it &lt;em&gt;without anyone having to remember to ask&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laravel Scalpel: the checks I wish had already existed
&lt;/h2&gt;

&lt;p&gt;The first piece is &lt;strong&gt;Laravel Scalpel&lt;/strong&gt;, a narrow, fast forensic scanner for Laravel codebases. It doesn't try to be a full SAST suite — it answers the exact three questions that would have shortened this incident from days to minutes: are there files sitting where they shouldn't be, does the environment configuration look right, are there permission or execution paths that shouldn't exist.&lt;/p&gt;

&lt;p&gt;It's picked up some traction beyond my own use case — featured by the Laravel Artisan Community, 24+ GitHub stars, and a pull request currently open to get it listed in &lt;code&gt;awesome-laravel&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  N8N Bastion: making sure someone's actually watching
&lt;/h2&gt;

&lt;p&gt;The second piece, &lt;strong&gt;N8N Bastion&lt;/strong&gt;, is what closes the real gap. It's a self-monitoring stack built on n8n that turns infrastructure and application problems into a Telegram message — instead of a support ticket that arrives after the damage is done. Laravel Scalpel now runs inside it as a scheduled check, not a tool anyone has to remember to invoke.&lt;/p&gt;

&lt;p&gt;Together they cover both halves of the problem: Scalpel knows what to look for on a Laravel app, and Bastion makes sure it's actually looking, on a schedule, without a human in the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worth open-sourcing
&lt;/h2&gt;

&lt;p&gt;PaketLebaranku.id will never make a breach headline. It's too small, too obscure, too ordinary — and that's exactly the point. There are a lot of small businesses running real, sensitive workloads on a single self-managed VPS with no monitoring at all, not because the owners don't care, but because every security tool out there quietly assumes a team and a budget they don't have.&lt;/p&gt;

&lt;p&gt;Laravel Scalpel and N8N Bastion exist because of one real incident on one real app. I'm putting both out there because I'd bet a lot of other small Laravel apps are sitting exactly where PaketLebaranku.id was, right before that 500 error showed up.&lt;/p&gt;

&lt;p&gt;If you run a Laravel app on a VPS you manage yourself, I'd genuinely like to know what breaks or what's missing if you try these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Laravel Scalpel: &lt;a href="https://github.com/hryagstn/laravel-scalpel" rel="noopener noreferrer"&gt;https://github.com/hryagstn/laravel-scalpel&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;N8N Bastion: &lt;a href="https://github.com/hryagstn/n8n-bastion" rel="noopener noreferrer"&gt;https://github.com/hryagstn/n8n-bastion&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
  </channel>
</rss>
