<?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: Achmad Junaedi</title>
    <description>The latest articles on DEV Community by Achmad Junaedi (@juna_go15).</description>
    <link>https://dev.to/juna_go15</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%2F4096507%2F720b974c-02b1-46bb-aeb3-d49b3f130679.jpg</url>
      <title>DEV Community: Achmad Junaedi</title>
      <link>https://dev.to/juna_go15</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/juna_go15"/>
    <language>en</language>
    <item>
      <title>Building a High-Speed School Marketplace (Part 2): Multi-Role Security with Supabase RLS</title>
      <dc:creator>Achmad Junaedi</dc:creator>
      <pubDate>Mon, 05 Oct 2026 05:05:01 +0000</pubDate>
      <link>https://dev.to/juna_go15/building-a-high-speed-school-marketplace-part-2-multi-role-security-with-supabase-rls-3db9</link>
      <guid>https://dev.to/juna_go15/building-a-high-speed-school-marketplace-part-2-multi-role-security-with-supabase-rls-3db9</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 2 of our technical series on building a high-speed, lightweight school marketplace. In Part 1: Why We Ditched Frameworks for Vanilla JS &amp;amp; PWA (&lt;a href="https://dev.to/juna_go15/building-a-high-speed-school-marketplace-why-we-ditched-frameworks-for-vanilla-js-pwa-27dl"&gt;https://dev.to/juna_go15/building-a-high-speed-school-marketplace-why-we-ditched-frameworks-for-vanilla-js-pwa-27dl&lt;/a&gt;), we discussed how we optimized the frontend for budget mobile devices. Today, we look at the backend architecture.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When planning the architecture for &lt;strong&gt;Aksara-Mart&lt;/strong&gt;—our digital marketplace for vocational school canteens and cooperatives—we faced a fundamental infrastructure question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do we really need to build, maintain, and scale a traditional custom backend server (like Node.js/Express, Python, or Go) just to shuttle data between the browser and the database?&lt;/strong&gt;&lt;br&gt;
For a school environment, maintaining a traditional backend comes with real trade-offs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Infrastructure &amp;amp; Hosting Overhead&lt;/strong&gt;: A dedicated server needs 24/7 uptime monitoring, memory management, and scaling configuration for peak recess traffic.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Maintenance Burden&lt;/strong&gt;: API routes, middleware controllers, and ORM abstractions require continuous patching and maintenance.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Unnecessary Latency&lt;/strong&gt;: Every simple query (browsing the lunch menu, checking an order status) has to make an extra hop through an intermediate API server layer.
We wanted an architecture that could &lt;strong&gt;minimize backend server workload&lt;/strong&gt; while keeping running costs close to zero.
That led us to &lt;strong&gt;Supabase&lt;/strong&gt;. By allowing our frontend JavaScript to communicate directly with PostgreSQL via auto-generated APIs and WebSockets, we could offload the bulk of routine read queries directly to the client.
However, letting client-side JavaScript query a database directly sounds like a security nightmare—unless you have a bulletproof authorization layer built into the database itself.
That is where &lt;strong&gt;PostgreSQL Row Level Security (RLS)&lt;/strong&gt; becomes the cornerstone of the entire system.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;The Architecture: Direct Client Queries Without the Middleman
In a traditional web application, every user request passes through three layers:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Browser (Client) ] ──▶ [ Custom API Server (Express/Node) ] ──▶ [ Database ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API server spends most of its CPU cycles doing mundane tasks: authenticating tokens, verifying if a user is allowed to read a row, running the query, and formatting JSON.&lt;br&gt;
With Supabase and PostgreSQL RLS, we simplified the architecture into a direct, high-performance two-tier model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Browser (Vanilla JS Client) ] ──────────────▶ [ Supabase PostgreSQL + RLS ]
• Direct catalog SELECT                         - Kernel-level row authorization
• Direct order history lookup                   - Instant query evaluation (&amp;lt; 3ms)
• Real-time WebSocket subscriptions             - Atomic RPC stored procedures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Advantages:&lt;br&gt;
• &lt;strong&gt;Zero Backend Server Maintenance&lt;/strong&gt;: No intermediate server processes to crash, restart, or run out of memory during a rush.&lt;br&gt;
• &lt;strong&gt;Minimal Compute Overhead&lt;/strong&gt;: PostgreSQL handles filtering and JSON serialization natively in C, which is vastly faster than parsing objects in Node.js.&lt;br&gt;
• &lt;strong&gt;Direct Client Queries&lt;/strong&gt;: Frontend JavaScript can fetch exactly the columns it needs, when it needs them, directly from the client.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Core Problem: Securing Direct Database Access
If frontend JavaScript talks directly to the database using a public API key, what prevents a student from querying other users' private orders or inspecting canteen cost margins?
This is where &lt;strong&gt;Row Level Security (RLS)&lt;/strong&gt; comes in.
Instead of writing authorization logic in backend middleware (&lt;code&gt;if (req.user.id !== order.user_id) return res.status(403)&lt;/code&gt;), the authorization rules are compiled directly into PostgreSQL table definitions.
• Without RLS: A client query &lt;code&gt;supabase.from('orders').select('*')&lt;/code&gt; returns every order in the database.
• With RLS: PostgreSQL automatically injects an internal filter into the query plan:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Automatically enforced by PostgreSQL on every query&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email_customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Even if a user inspects the network tab and writes a custom script asking for all records, the database engine guarantees they only receive the rows they are explicitly permitted to see.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Managing Roles Across the School Ecosystem
A school marketplace serves several distinct groups with different permissions:
| Role | Access Scope | How Queries Are Handled |
| :--- | :--- | :--- |
| &lt;strong&gt;&lt;code&gt;siswa&lt;/code&gt; (Students)&lt;/strong&gt; | Browse active products, view own order history. | Direct client &lt;code&gt;SELECT&lt;/code&gt; constrained by RLS. |
| &lt;strong&gt;&lt;code&gt;guru&lt;/code&gt; (Teachers)&lt;/strong&gt; | Same as students, with priority fulfillment flags. | Direct client &lt;code&gt;SELECT&lt;/code&gt; constrained by RLS. |
| &lt;strong&gt;&lt;code&gt;kasir&lt;/code&gt; (Cashiers)&lt;/strong&gt; | View all active incoming orders, print receipts. | Direct client &lt;code&gt;SELECT&lt;/code&gt; with staff privileges; no direct price/stock editing. |
| &lt;strong&gt;&lt;code&gt;manajer&lt;/code&gt; (Managers)&lt;/strong&gt; | Manage catalog items, update stock, view revenue. | Full CRUD permissions on product tables via RLS. |
| &lt;strong&gt;&lt;code&gt;admin&lt;/code&gt; (System Admins)&lt;/strong&gt; | System-wide configuration and role management. | Full administrative access. |
The &lt;code&gt;admin_role&lt;/code&gt; Table
To keep role checks clean and reliable, we maintain a dedicated role table in PostgreSQL:
&lt;/li&gt;
&lt;/ol&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="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admin_role&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&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;span class="k"&gt;role&lt;/span&gt; &lt;span class="nb"&gt;TEXT&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;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admin_role&lt;/span&gt; &lt;span class="n"&gt;ENABLE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Practical Gotchas We Addressed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Case-Insensitive Emails&lt;/strong&gt;: Mobile keyboards often capitalize the first letter of an email (&lt;code&gt;User@school.edu&lt;/code&gt;). Always use &lt;code&gt;lower(trim(email))&lt;/code&gt; in policies to prevent login lockouts.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dual-Spelling Tolerance&lt;/strong&gt;: In bilingual environments, staff may enter roles in English or Indonesian (&lt;code&gt;manager&lt;/code&gt; vs &lt;code&gt;manajer&lt;/code&gt;, &lt;code&gt;cashier&lt;/code&gt; vs &lt;code&gt;kasir&lt;/code&gt;). We check role aliases with &lt;code&gt;IN ('manager', 'manajer', 'cashier', 'kasir')&lt;/code&gt; so minor spelling differences don't break authentication.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Separation of Concerns: Direct Reads vs Server-Side Writes&lt;br&gt;
Allowing frontend JavaScript to query the database directly works brilliantly for &lt;strong&gt;reads&lt;/strong&gt;, but transactional &lt;strong&gt;writes&lt;/strong&gt; require a different boundary.&lt;br&gt;
A. Direct Client Reads (Safe via RLS)&lt;br&gt;
Frontend JavaScript directly reads from the database for:&lt;br&gt;
• &lt;strong&gt;The product catalog&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Direct client query in skripmart.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;product_list&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id, name, price, stock, image_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;RLS ensures that wholesale costs (&lt;code&gt;modal_price&lt;/code&gt;) and archived items are filtered out before data leaves PostgreSQL.&lt;br&gt;
• &lt;strong&gt;Order tracking&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Customers only receive their own orders&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;myOrders&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ascending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;B. Controlled Server-Side Writes (Via Database RPC)&lt;br&gt;
While reading data directly from JavaScript is safe, &lt;strong&gt;creating orders and modifying stock should never happen via direct client &lt;code&gt;INSERT&lt;/code&gt; or &lt;code&gt;UPDATE&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ ANTI-PATTERN: Never allow direct client inserts for financial transactions&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="na"&gt;total_price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clientCalculatedTotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Client could tamper with this value!&lt;/span&gt;
&lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cartItems&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we allowed direct client inserts on the &lt;code&gt;orders&lt;/code&gt; table, a user could theoretically alter prices or bypass stock limits.&lt;br&gt;
Instead, we follow a simple architectural rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Frontend JavaScript reads data directly using RLS.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Frontend JavaScript writes transactions via PostgreSQL Stored Procedures (RPC).&lt;/strong&gt;&lt;br&gt;
When a student checks out, the client invokes an RPC procedure (&lt;code&gt;v2_create_order&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ SECURE: Transaction logic runs atomically inside PostgreSQL&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v2_create_order&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="na"&gt;p_customer_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;p_delivery_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;deliveryOption&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;p_items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cartPayload&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside this procedure, the database recalculates prices from the official catalog, checks remaining stock, decrements inventory, and creates the order records in a single atomic transaction.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What We Gained from This Architecture
By offloading query processing directly to the client and database engine:&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Zero Backend Maintenance&lt;/strong&gt;: We do not maintain or pay for a custom Node.js/Express server layer.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Sub-3ms Authorization Overhead&lt;/strong&gt;: RLS policy checks happen natively within PostgreSQL's query optimizer with virtually no latency penalty.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;High Reliability During Traffic Spikes&lt;/strong&gt;: When hundreds of students browse the menu simultaneously during recess, Supabase and Cloudflare serve the requests directly without choking an intermediate web server.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Complete Data Privacy&lt;/strong&gt;: Every customer and staff member only receives data that belongs to their assigned role.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What's Next in Part 3?&lt;br&gt;
With direct reads secured by RLS and client-side queries running smoothly, we still had to solve one critical concurrency problem:&lt;br&gt;
During the 15-minute school break, &lt;strong&gt;dozens of students click "Checkout" at the exact same millisecond for the last few items in the canteen&lt;/strong&gt;.&lt;br&gt;
How do we prevent &lt;strong&gt;race conditions and overselling&lt;/strong&gt; without a heavy application server?&lt;br&gt;
&lt;strong&gt;In Part 3: Atomic Checkouts &amp;amp; Zero Overselling with PostgreSQL RPC&lt;/strong&gt;, we will break down how we implemented &lt;strong&gt;pessimistic row locking (&lt;code&gt;FOR UPDATE&lt;/code&gt;)&lt;/strong&gt; inside PostgreSQL to handle high-concurrency checkout spikes in &lt;strong&gt;under 42 milliseconds&lt;/strong&gt;!&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
      <category>security</category>
    </item>
    <item>
      <title>Ditched React for our school marketplace app 👇 Bundle: &lt;50KB. Load time on a $80 Android: 320ms. Story + architecture breakdown here #DEVCommunity #webdev</title>
      <dc:creator>Achmad Junaedi</dc:creator>
      <pubDate>Wed, 09 Sep 2026 08:44:07 +0000</pubDate>
      <link>https://dev.to/juna_go15/ditched-react-for-our-school-marketplace-app-bundle-50kb-load-time-on-a-80-android-320ms-1dnl</link>
      <guid>https://dev.to/juna_go15/ditched-react-for-our-school-marketplace-app-bundle-50kb-load-time-on-a-80-android-320ms-1dnl</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/juna_go15/building-a-high-speed-school-marketplace-why-we-ditched-frameworks-for-vanilla-js-pwa-27dl" class="crayons-story__hidden-navigation-link"&gt;Building a High-Speed School Marketplace: Why We Ditched Frameworks for Vanilla JS &amp;amp; PWA&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/juna_go15" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4096507%2F720b974c-02b1-46bb-aeb3-d49b3f130679.jpg" alt="juna_go15 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/juna_go15" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Achmad Junaedi
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Achmad Junaedi
                
                
              
              &lt;div id="story-author-preview-content-4612886" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/juna_go15" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4096507%2F720b974c-02b1-46bb-aeb3-d49b3f130679.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Achmad Junaedi&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/juna_go15/building-a-high-speed-school-marketplace-why-we-ditched-frameworks-for-vanilla-js-pwa-27dl" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 9&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/juna_go15/building-a-high-speed-school-marketplace-why-we-ditched-frameworks-for-vanilla-js-pwa-27dl" id="article-link-4612886"&gt;
          Building a High-Speed School Marketplace: Why We Ditched Frameworks for Vanilla JS &amp;amp; PWA
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/pwa"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;pwa&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/juna_go15/building-a-high-speed-school-marketplace-why-we-ditched-frameworks-for-vanilla-js-pwa-27dl" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/juna_go15/building-a-high-speed-school-marketplace-why-we-ditched-frameworks-for-vanilla-js-pwa-27dl#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>architecture</category>
      <category>javascript</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a High-Speed School Marketplace: Why We Ditched Frameworks for Vanilla JS &amp; PWA</title>
      <dc:creator>Achmad Junaedi</dc:creator>
      <pubDate>Wed, 09 Sep 2026 08:08:41 +0000</pubDate>
      <link>https://dev.to/juna_go15/building-a-high-speed-school-marketplace-why-we-ditched-frameworks-for-vanilla-js-pwa-27dl</link>
      <guid>https://dev.to/juna_go15/building-a-high-speed-school-marketplace-why-we-ditched-frameworks-for-vanilla-js-pwa-27dl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5tqkckzl916fyvzqo9h6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5tqkckzl916fyvzqo9h6.webp" alt=" " width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is Part 1 of a 4-part series on building a high-speed, race-condition-proof digital marketplace for schools using Vanilla JS, Supabase RLS, PostgreSQL RPC, and Cloudflare Workers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the modern web ecosystem, starting any web project without reaching for React, Next.js, or Vue has almost become taboo. &lt;/p&gt;

&lt;p&gt;Yet, when our team at &lt;strong&gt;&lt;a href="https://setvy.id" rel="noopener noreferrer"&gt;Setvy&lt;/a&gt;&lt;/strong&gt; sat down to architect &lt;strong&gt;Aksara-Mart&lt;/strong&gt;—a digital marketplace serving thousands of students and teachers across our vocational high school—we made an intentional, contrarian decision:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;We threw out the frontend frameworks and built the entire client in pure Vanilla JavaScript, modern CSS, and PWA standards.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why? Because when your user base consists of students using second-hand, $80 Android phones running on limited prepaid mobile data, developer convenience cannot come at the expense of end-user performance.&lt;/p&gt;

&lt;p&gt;Here is what we learned, why it paid off, and how you can implement a high-speed Vanilla JS architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Reality of School Hardware
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of code, we profiled our target environment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hardware Constraints&lt;/strong&gt;: 65% of students carry entry-level devices with 2GB to 3GB of RAM, powered by aging Quad-core MediaTek or Snapdragon 400-series processors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prepaid Data, Not Wi-Fi&lt;/strong&gt;: Most students don't rely on school Wi-Fi at all—they browse on their own prepaid mobile data plans (&lt;em&gt;kuota&lt;/em&gt;). Every megabyte is paid out of their pocket money, so a heavy page literally costs them. On top of that, when hundreds of phones hit the same nearby cell tower during recess—from inside concrete classroom buildings—effective speeds often drop to 2G/3G levels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Recess Rush&lt;/strong&gt;: Students have a very brief window between bells to browse the canteen menu, order food, and pick it up. A 4-second loading screen means they don't eat.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Modern Framework Tax on Budget Devices
&lt;/h3&gt;

&lt;p&gt;A barebones Next.js or React e-commerce template typically ships:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;250 KB – 500 KB&lt;/strong&gt; of minified JavaScript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript Parsing &amp;amp; Compilation Time&lt;/strong&gt;: On an iPhone 16 or M3 MacBook, parsing 300KB of JS takes 15 milliseconds. On a budget Android phone, it takes &lt;strong&gt;1,200 to 2,500 milliseconds&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hydration Lag (Total Blocking Time)&lt;/strong&gt;: The page &lt;em&gt;looks&lt;/em&gt; loaded, but clicking the "Add to Cart" button does nothing because the JavaScript thread is frozen during hydration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On high-end dev laptops, this latency is invisible. On budget devices, it ruins the experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Architecture: Under 50 KB, 60 FPS
&lt;/h2&gt;

&lt;p&gt;By returning to web fundamentals, we achieved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total Initial JS + CSS Payload&lt;/strong&gt;: &lt;strong&gt;&amp;lt; 48 KB&lt;/strong&gt; (gzipped).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First Contentful Paint (FCP)&lt;/strong&gt;: &lt;strong&gt;~320 ms&lt;/strong&gt; on low-end 4G.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to Interactive (TTI)&lt;/strong&gt;: &lt;strong&gt;Instantaneous&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is how our client structure looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aksaramart/
├── public/
│   ├── index.html       # Semantic HTML5 shell &amp;amp; modal templates
│   ├── skripmart.js     # Modular Vanilla JS logic (~35 KB)
│   ├── stylemart.css    # Clean, framework-free CSS with CSS Variables
│   ├── sw.js            # PWA Service Worker for caching &amp;amp; offline resilience
│   └── manifest.json    # Progressive Web App configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Clean Patterns for Vanilla JS State &amp;amp; DOM
&lt;/h2&gt;

&lt;p&gt;Building with Vanilla JS doesn't mean writing chaotic spaghetti code. We used modern ES6+ patterns, event delegation, and atomic state rendering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern A: Modular State Store
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A lightweight, reactive-free state store&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aksara_cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;activeCategory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveCart&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aksara_cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;renderCartBadge&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addToCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;appState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;appState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;appState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;saveCart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;renderCartUI&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern B: High-Performance Event Delegation
&lt;/h3&gt;

&lt;p&gt;Instead of attaching individual click listeners to hundreds of catalog items, we attached a single listener to the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;catalogGrid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;btn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-action="add-to-cart"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;addToCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Smooth micro-animation without any animation library&lt;/span&gt;
    &lt;span class="nx"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scale-bounce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;btn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scale-bounce&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Making It a True App: Progressive Web App (PWA)
&lt;/h2&gt;

&lt;p&gt;Most students won't go to Google Play or the App Store to install a canteen app. But with &lt;strong&gt;PWA standards&lt;/strong&gt;, they can install it directly from the browser in 1 tap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Web App Manifest (&lt;code&gt;manifest.json&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Aksara Mart - Kantin &amp;amp; Koperasi Sekolah"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"short_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AksaraMart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"display"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"standalone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"background_color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#ffffff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"theme_color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#10b981"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"icons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/icons/icon-192.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192x192"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/icons/icon-512.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"512x512"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Custom "Install App" Topbar Trigger
&lt;/h3&gt;

&lt;p&gt;Browsers often show a subtle or clunky native install banner. We intercepted the &lt;code&gt;beforeinstallprompt&lt;/code&gt; event to give students an explicit, high-visibility install button in the top navigation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;deferredPrompt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;installBtn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;installAppBtn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beforeinstallprompt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Prevent default mini-infobar&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;deferredPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// Show our custom branded install button&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;installBtn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;installBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inline-flex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;installBtn&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;deferredPrompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;deferredPrompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;outcome&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;deferredPrompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userChoice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`User install outcome: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;deferredPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;installBtn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Cache-First Service Worker (&lt;code&gt;sw.js&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;To guarantee instant loading even when the cellular signal drops (or a student's data quota runs out mid-week), our Service Worker caches core static shell assets—so repeat visits cost almost zero data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CACHE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aksaramart-v1.4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STATIC_ASSETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/skripmart.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/stylemart.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/manifest.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/icons/icon-192.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CACHE_NAME&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STATIC_ASSETS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;skipWaiting&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Only cache static GET requests, let API requests go to network&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/rest/v1/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/rpc/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. The Performance Verdict
&lt;/h2&gt;

&lt;p&gt;Here is the Lighthouse audit comparison run on an emulated Moto G4 with a throttled 3G network:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Typical Next.js / React E-Commerce&lt;/th&gt;
&lt;th&gt;Aksara-Mart (Vanilla JS + PWA)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance Score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;62 / 100&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99 / 100&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;First Contentful Paint (FCP)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.9 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.34 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Blocking Time (TBT)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;840 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Speed Index&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.4 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.6 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Transfer Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;380 KB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;46 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  6. Conclusion
&lt;/h2&gt;

&lt;p&gt;Ditching frameworks for Vanilla JS proved to be a resounding success for our students and teachers. The app launches immediately, remains completely immune to hydration freezes, and preserves scarce phone battery and memory.&lt;/p&gt;

&lt;p&gt;However, a fast frontend is completely meaningless if the underlying database is vulnerable. How do we secure transaction data from curious IT vocational students who open &lt;code&gt;Inspect Element / DevTools&lt;/code&gt; to try altering menu prices from $1.50 to $0.01?&lt;/p&gt;

&lt;p&gt;In the next article, we'll dive deep into our Zero-Trust database security architecture using &lt;strong&gt;Supabase Row Level Security (RLS)&lt;/strong&gt; and multi-role governance.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>pwa</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Stopped Using Gemini 3.1 Pro for Agentic Coding — Why I Moved to Gemini 3.7 Flash High Mode</title>
      <dc:creator>Achmad Junaedi</dc:creator>
      <pubDate>Tue, 01 Sep 2026 11:32:50 +0000</pubDate>
      <link>https://dev.to/juna_go15/i-stopped-using-gemini-31-pro-for-agentic-coding-why-i-moved-to-gemini-37-flash-high-mode-43hf</link>
      <guid>https://dev.to/juna_go15/i-stopped-using-gemini-31-pro-for-agentic-coding-why-i-moved-to-gemini-37-flash-high-mode-43hf</guid>
      <description>&lt;p&gt;I landed my first Point of Sale (POS) SaaS client project and built it inside &lt;strong&gt;Google Antigravity&lt;/strong&gt;, trusting &lt;strong&gt;Gemini 3.1 Pro&lt;/strong&gt; for all agentic coding. Bad move. Midway through, severe bugs hit—fixing sales inputs completely broke my dashboard analytics.&lt;/p&gt;

&lt;p&gt;Frustrated, I switched to the GPT stack (Codex, GPT-5.5, GPT-5.6 Sol). But that brought a new headache: token quotas drained in minutes and API bills went through the roof.&lt;/p&gt;

&lt;p&gt;The breakthrough? Community stats on Agent Arena showed Flash models beating 3.1 Pro in agentic tasks. Desperate, I tested &lt;strong&gt;Gemini 3.6 Flash&lt;/strong&gt; and it was surprisingly solid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The Beginning: Falling for 3.1 Pro Hype&lt;/strong&gt;&lt;br&gt;
When Google dropped Gemini 3.1 Pro in February 2026, tech Twitter hyped its ARC-AGI-2 benchmark scores as the ultimate problem solver.&lt;br&gt;
Early on, 3.1 Pro handled high-level whiteboard concepts fine:&lt;br&gt;
• &lt;strong&gt;System Architecture&lt;/strong&gt;: Mapping schemas and ERDs.&lt;br&gt;
• &lt;strong&gt;Business Logic&lt;/strong&gt;: Outlining business logic flows.&lt;br&gt;
• &lt;strong&gt;Edge Cases&lt;/strong&gt;: Spotting potential edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Client Pressure &amp;amp; Regression Hell&lt;/strong&gt;&lt;br&gt;
I secured a major deal with a client who paid 50% upfront. Expectations were high, and that's where things went downhill.&lt;br&gt;
A retail POS requires tight database relations: transactions, inventory sync, multi-outlet stock, and live dashboard revenue summaries. When I instructed Gemini 3.1 Pro in Antigravity to build the sales input module:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fatal Regressions:&lt;/strong&gt; The sales input worked, but the dashboard suddenly went crazy. The model rewrote existing SQL aggregation queries without respecting cross-module dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lost Query Logic&lt;/strong&gt;: It repeatedly forgot critical database filter conditions (like tenant IDs or soft-delete flags), causing messy data anomalies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deadline Panic&lt;/strong&gt;: The client wanted deliverables per contract, and I was sweating because the code kept breaking.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;3. Fleeing to GPT &amp;amp; The Token Trap&lt;/strong&gt;&lt;br&gt;
Panicking about deadlines, I switched to the OpenAI ecosystem:&lt;br&gt;
• &lt;strong&gt;Codex / GPT-5.5 (Medium)&lt;/strong&gt;: Daily coding assistant.&lt;br&gt;
• &lt;strong&gt;GPT-5.5 (High Reasoning)&lt;/strong&gt;: Complex SQL logic.&lt;br&gt;
• &lt;strong&gt;GPT-5.6 Sol&lt;/strong&gt;: Heavy artillery for deep debugging.&lt;br&gt;
While reasoning was solid, a new problem popped up: tokens burned out insanely fast.&lt;/p&gt;

&lt;p&gt;Running heavy models like GPT-5.6 Sol and 5.5 High across autonomous agentic loops burned quotas in no time. I needed a workhorse that didn't drain my wallet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Community Outrage: It Wasn't Just Me&lt;/strong&gt;&lt;br&gt;
My frustration wasn't unique. On the &lt;a href="https://discuss.ai.google.dev/t/unacceptable-antigravity-quotas-for-gemini-3-1-pro-workflow-completely-blocked/124971" rel="noopener noreferrer"&gt;Google AI Developers Forum&lt;/a&gt; , a thread titled "Unacceptable Antigravity Quotas for Gemini 3.1 Pro – Workflow Completely Blocked!" had racked up roughly 60 upvotes and 19 replies within days.&lt;/p&gt;

&lt;p&gt;One developer, Hauke_Walden, summed up the core complaint: "...it is totally unacceptable that the quota usage is so intransparent, and even worse, the totally random 'refresh' policy... It's like a car that sometimes works, sometimes it doesn't. That's ok for a hobbyist and his vintage car, but not for a cab driver."&lt;/p&gt;

&lt;p&gt;Another, madnz-08, described a specific bug: "Yesterday my quota dropped from 60% to 0% without any prompt, and the worst part was seeing the reset time set to 50+ hours."&lt;/p&gt;

&lt;p&gt;A third, MrTos, put it bluntly: "It is unacceptable to be stuck at 10 prompts on Gemini 3.1."&lt;/p&gt;

&lt;p&gt;The pattern in the thread was consistent: vanishing quotas, lockout timers reported as high as 133–167 hours, and reports of paying developers getting locked out mid-project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The Pivot: Testing Gemini 3.6 Flash&lt;/strong&gt;&lt;br&gt;
Hunting for a leaner alternative, I saw rankings on Agent Arena praising Flash models for agentic tool use.&lt;br&gt;
Curious, I tested Gemini 3.6 Flash in Antigravity:&lt;br&gt;
• &lt;strong&gt;Blazing Speed&lt;/strong&gt;: Instant feedback loops without lag.&lt;br&gt;
• &lt;strong&gt;Quota Sanity&lt;/strong&gt;: No panic about hitting rate limits mid-session.&lt;br&gt;
• &lt;strong&gt;Sane Diffs&lt;/strong&gt;: It stayed in its lane without aggressively trashing outside files.&lt;br&gt;
It proved lightweight models were getting unwarranted hate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. August: Brother's WhatsApp Tip &amp;amp; The Jump to 3.7 Flash&lt;/strong&gt;&lt;br&gt;
In August, my younger brother sent me a WhatsApp message: "Bro, Google just dropped Gemini 3.7 Flash."&lt;br&gt;
As reported by &lt;a href="https://venturebeat.com/technology/googles-gemini-3-7-flash-targets-coding-and-agents-with-a-50-introductory-price-cut" rel="noopener noreferrer"&gt;VentureBeat&lt;/a&gt;, Google  built 3.7 Flash specifically for coding and agentic loops, with a 50% introductory discount ($0.75 / 1M input, $3.75 / 1M output).&lt;br&gt;
Having seen good results with 3.6 Flash, I wanted to see if Google really pulled off deep reasoning on a Flash model without killing speed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. The Proof: Gemini 3.7 Flash (High Mode) in Antigravity&lt;/strong&gt;&lt;br&gt;
I opened Antigravity, picked Gemini 3.7 Flash, and enabled Thinking Mode: High. The leap in quality was immediate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blazing Fast &amp;amp; Token-Efficient&lt;/strong&gt; Inference stayed snappy even with extended thinking on. In fast agent loops, code fixes felt instant without distracting pauses, while token burn remained low.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex Database Queries Handled Smoothly&lt;/strong&gt; Any fear that Flash would choke on multi-table queries disappeared. It surgically edited the sales module without touching outside files, kept dashboard queries in sync, and maintained schema rules without destructive side effects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleaner Code &amp;amp; Only Minor Tweaks&lt;/strong&gt; There were a few minor formatting adjustments, but the code structure was clean. Those small tweaks took one quick prompt to fix—not fatal structural bugs. I delivered the client project right on time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
This client project taught me a valuable lesson: in daily agentic coding, diff discipline, fast inference, and token efficiency matter way more than benchmark scores on paper.&lt;br&gt;
From Agent Arena tests with 3.6 Flash to the leap in Gemini 3.7 Flash, it proved Flash models with thinking mode aren't just "cheap alternatives"—they are solid workhorses that handle heavy code cleanly and safely.&lt;br&gt;
If you're building a SaaS and stuck between regression bugs or sky-high token bills, give Gemini 3.7 Flash a run in Google Antigravity.&lt;/p&gt;

&lt;p&gt;Written by Achmad Junaedi — Founder of &lt;a href="https://setvy.id/" rel="noopener noreferrer"&gt;setvy.id&lt;/a&gt; &amp;amp; Vocational Educator at SMKN 10 Surabaya, Indonesia.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>saas</category>
      <category>database</category>
    </item>
  </channel>
</rss>
