<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://adriandevera.vercel.app/</id>
    <title>Adrian De Vera - Portfolio</title>
    <updated>2026-08-12T23:59:54.586Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <author>
        <name>Adrian De Vera</name>
        <email>deveraadrian46@gmail.com</email>
        <uri>https://adriandevera.vercel.app</uri>
    </author>
    <link rel="alternate" href="https://adriandevera.vercel.app/"/>
    <subtitle>Lead AI Systems &amp; Full Stack Engineer specializing in scalable web applications and autonomous agentic workflows with Next.js, TypeScript, Supabase, and AI agents.</subtitle>
    <icon>https://adriandevera.vercel.app/favicon.svg</icon>
    <rights>All rights reserved 2026, Adrian De Vera</rights>
    <entry>
        <title type="html"><![CDATA[Free Antigravity Models in Hermes]]></title>
        <id>https://adriandevera.vercel.app/hermes-antigravity-blog</id>
        <link href="https://adriandevera.vercel.app/hermes-antigravity-blog"/>
        <updated>2026-08-09T16:00:00.000Z</updated>
        <summary type="html"><![CDATA[How I got Hermes Agent talking to Google's Antigravity (Cloud Code Assist) for free — and the bizarre bug that almost made me give up.]]></summary>
        <content type="html"><![CDATA[<main class="notion light-mode notion-page notion-block-3b7dfb2b64f88008abb3dbd978c26f27"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><h2 class="notion-h notion-h1 notion-h-indent-0 notion-block-3b7dfb2b64f8809192fcfbd656f53905" data-id="3b7dfb2b64f8809192fcfbd656f53905"><span><div id="3b7dfb2b64f8809192fcfbd656f53905" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8809192fcfbd656f53905" title="Integrating Google Antigravity Models into Hermes Agent: The Complete Guide &amp; Engineering Story"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Integrating Google Antigravity Models into Hermes Agent: The Complete Guide &amp; Engineering Story</span></span></h2><div class="notion-text notion-block-3b7dfb2b64f88006ad98e98c62ee0ca3"><em>How to get free access to Gemini 3.1 Pro, Gemini 3.5 Flash, Gemini 3.6 Flash, and Claude 4.6 inside Hermes Agent—and how to fix the hidden schema, identity filter, and provider routing bugs.</em></div><hr class="notion-hr notion-block-3b7dfb2b64f880e4a3a2fd1534020015"/><blockquote class="notion-quote notion-block-3b7dfb2b64f8802ca923ce0437ecd305"><div>💡 <b>TL;DR</b>: Google’s <b>Antigravity</b> (Cloud Code Assist) offers generous free tiers for frontier models (Gemini 3.x, Claude 4.6, GPT-OSS 120B). Integrating these models into <b>Hermes Agent</b> via the <code class="notion-inline-code">antigravity-provider</code> plugin requires solving four distinct engineering challenges:</div><ol start="1" class="notion-list notion-list-numbered notion-block-3b7dfb2b64f880ce8eecda990d09bb49"><li><b>Schema Translation</b>: Sanitizing standard JSON Schema keywords (<code class="notion-inline-code">const</code>, <code class="notion-inline-code">oneOf</code>, <code class="notion-inline-code">$ref</code>) for Gemini&#x27;s strict <code class="notion-inline-code">functionDeclarations</code>.</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-3b7dfb2b64f8804c9405c1112f510ba5"><li><b>Identity Scrubbing</b>: Neutralizing a secret Google upstream prompt classifier that returns a masked <code class="notion-inline-code">429 RESOURCE_EXHAUSTED</code> whenever system instructions contain <em>&quot;You are Hermes Agent...&quot;</em>.</li></ol><ol start="3" class="notion-list notion-list-numbered notion-block-3b7dfb2b64f88030a84fde820abd710b"><li><b>Provider Resolution</b>: Bridging Hermes’ runtime plugin registry with <code class="notion-inline-code">resolve_provider_full()</code> to eliminate <code class="notion-inline-code">Unknown provider &#x27;antigravity&#x27;</code> errors during model switching.</li></ol><ol start="4" class="notion-list notion-list-numbered notion-block-3b7dfb2b64f880e49ad6e62d16ca8a9d"><li><b>Wire Model Mapping</b>: Dynamically mapping reasoning effort levels (<code class="notion-inline-code">low</code>, <code class="notion-inline-code">medium</code>, <code class="notion-inline-code">high</code>) to Google&#x27;s expected wire model IDs (e.g. <code class="notion-inline-code">gemini-3.6-flash-high</code>).</li></ol><div class="notion-text notion-block-3b7dfb2b64f880708d0dd2abdb0825a2">This guide covers the complete setup, architectural breakdown, and step-by-step fixes to get Antigravity running seamlessly in Hermes.</div></blockquote><hr class="notion-hr notion-block-3b7dfb2b64f8809bb06aed71fd1799f8"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f8807e8b06f1bad827dcc6" data-id="3b7dfb2b64f8807e8b06f1bad827dcc6"><span><div id="3b7dfb2b64f8807e8b06f1bad827dcc6" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8807e8b06f1bad827dcc6" title="1. Introduction: Why Google Antigravity + Hermes Agent?"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">1. Introduction: Why Google Antigravity + Hermes Agent?</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f88019ae0cc4424eb8832f">If you use tools like <b>omp (Oh My Pi)</b>, you are likely familiar with Google Antigravity: free, high-throughput access to frontier AI models including Gemini 3.1 Pro, Gemini 3.5 Flash, Gemini 3.6 Flash, Claude 4.6 (via Google), and GPT-OSS 120B.</div><div class="notion-text notion-block-3b7dfb2b64f88026b8efc7b5d803edd7">For developers who use <b>Hermes Agent</b> (by Nous Research) as their primary terminal coding assistant, bringing Antigravity into Hermes is a huge win—it unlocks powerful, zero-cost reasoning and coding models directly inside Hermes’ rich tool-calling and memory environment.</div><div class="notion-text notion-block-3b7dfb2b64f880b9b97fe69d8da17b0f">Hermes supports custom model providers via plugins. The <code class="notion-inline-code">antigravity-provider</code> plugin serves as an in-process bridge, translating OpenAI-formatted chat completions into Google’s Cloud Code Assist API (<code class="notion-inline-code">v1internal:streamGenerateContent</code>).</div><div class="notion-text notion-block-3b7dfb2b64f8801bb6eec5a2e3b9b870">However, bringing these two systems together out of the box leads to immediate failures. Here is the full guide to getting it working.</div><hr class="notion-hr notion-block-3b7dfb2b64f880bf93b1c63d4bfdfbd0"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f88022b0b3e20fab546d43" data-id="3b7dfb2b64f88022b0b3e20fab546d43"><span><div id="3b7dfb2b64f88022b0b3e20fab546d43" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88022b0b3e20fab546d43" title="2. Quickstart &amp; Installation Setup"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">2. Quickstart &amp; Installation Setup</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f880c8853cffc8fb88a1da">Getting the plugin running requires three setup steps:</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f8802380e3d6a7ef750c5c" data-id="3b7dfb2b64f8802380e3d6a7ef750c5c"><span><div id="3b7dfb2b64f8802380e3d6a7ef750c5c" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8802380e3d6a7ef750c5c" title="Step 1: Install the Plugin in Editable Mode"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Step 1: Install the Plugin in Editable Mode</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f880aaad21fa7456f05f01">Because the plugin repository (<code class="notion-inline-code">~/.hermes/plugins/antigravity-provider</code>) uses a Python <code class="notion-inline-code">src/</code> layout (<code class="notion-inline-code">src/antigravity_provider</code>), Hermes&#x27; default launcher shim may fail to locate the package:</div><div class="notion-text notion-block-3b7dfb2b64f8804fa4eae8dee7b5f7ff">This binds the plugin directly into Hermes&#x27; venv, ensuring imports persist across authentication refreshes.</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f8809295d6f1808902fd30" data-id="3b7dfb2b64f8809295d6f1808902fd30"><span><div id="3b7dfb2b64f8809295d6f1808902fd30" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8809295d6f1808902fd30" title="Step 2: Authenticate OAuth Credentials"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Step 2: Authenticate OAuth Credentials</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f8807e95e2ebe8258e82b0">Authenticate your Google account using <code class="notion-inline-code">hermes agy login</code>:</div><div class="notion-text notion-block-3b7dfb2b64f880c6b4d5e56dd8e2099b">This retrieves your OAuth tokens and stores them securely in the macOS Keychain or <code class="notion-inline-code">~/.hermes/</code>.</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f8800ca1fdcd0aed5e0b8c" data-id="3b7dfb2b64f8800ca1fdcd0aed5e0b8c"><span><div id="3b7dfb2b64f8800ca1fdcd0aed5e0b8c" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8800ca1fdcd0aed5e0b8c" title="Step 3: Configure config.yaml"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Step 3: Configure <code class="notion-inline-code">config.yaml</code></span></span></h4><div class="notion-text notion-block-3b7dfb2b64f880498061f58d5351a59c">Add the <code class="notion-inline-code">antigravity</code> provider block to <code class="notion-inline-code">~/.hermes/config.yaml</code>:</div><hr class="notion-hr notion-block-3b7dfb2b64f88037b857eb4f69ba1054"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f8804282f2ffaaf412d77f" data-id="3b7dfb2b64f8804282f2ffaaf412d77f"><span><div id="3b7dfb2b64f8804282f2ffaaf412d77f" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8804282f2ffaaf412d77f" title="3. Engineering Deep Dive: Overcoming the Integration Hurdles"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">3. Engineering Deep Dive: Overcoming the Integration Hurdles</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f8806aae4fce5943c290aa">Connecting Hermes to Antigravity isn&#x27;t just a matter of passing credentials—it requires adapting Hermes&#x27; OpenAI-style structures to Google&#x27;s specialized Cloud Code Assist backend.</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f880b2b88de799a0a6ab1f" data-id="3b7dfb2b64f880b2b88de799a0a6ab1f"><span><div id="3b7dfb2b64f880b2b88de799a0a6ab1f" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f880b2b88de799a0a6ab1f" title="Challenge 1: Translating Tool JSON Schemas"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Challenge 1: Translating Tool JSON Schemas</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f880f8a1baf0ce020e2adf">When Hermes executes an agent session, it exports JSON Schemas for its active tools. Standard Hermes schemas use modern JSON Schema features (<code class="notion-inline-code">const</code>, <code class="notion-inline-code">oneOf</code>, <code class="notion-inline-code">anyOf</code>, <code class="notion-inline-code">$ref</code>, <code class="notion-inline-code">definitions</code>, <code class="notion-inline-code">additionalProperties</code>).</div><div class="notion-text notion-block-3b7dfb2b64f880bd8ac3cf6a5e7859d3">Google’s Gemini API expects minimal OpenAPI / Gemini schema objects. Sending standard Hermes schemas results in HTTP 400 validation rejections:</div><div class="notion-text notion-block-3b7dfb2b64f8803db50ddc66c0dfb0eb"><b>Solution</b>: Update <code class="notion-inline-code">_schema()</code> in <code class="notion-inline-code">transform.py</code> to recursively strip non-compliant schema keywords before translating tools upstream:</div><hr class="notion-hr notion-block-3b7dfb2b64f880fea92ac2f0727179f7"/><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f880dba015e86bca5d44eb" data-id="3b7dfb2b64f880dba015e86bca5d44eb"><span><div id="3b7dfb2b64f880dba015e86bca5d44eb" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f880dba015e86bca5d44eb" title="Challenge 2: Unmasking the Identity 429 Filter"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Challenge 2: Unmasking the Identity 429 Filter</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f880399c4df154dfedbf81">With schemas sanitized, requests reached Google’s API... but every full agent turn immediately failed with a <code class="notion-inline-code">429 RESOURCE_EXHAUSTED</code> error:</div><div class="notion-text notion-block-3b7dfb2b64f880d2891ef86254228c97">A standard HTTP 429 implies rate limits or zero quota. However, testing the exact same OAuth credentials inside <code class="notion-inline-code">omp</code> succeeded instantly.</div><div></div><div class="notion-text notion-block-3b7dfb2b64f8800f8b6bdac800ecb32d">I extracted the OAuth tokens and tested requests using Hermes&#x27; request builder:</div><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88033aa32e066d062b9ce"><li><b>Minimal Request</b> (<code class="notion-inline-code">&quot;Reply with: OK&quot;</code>): <b>HTTP 200 OK</b></li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f880519b06cd2ff2660eff"><li><b>Full Agent Request</b> (19KB system prompt + 36 tool definitions): <b>HTTP 429</b></li></ul><div class="notion-text notion-block-3b7dfb2b64f88059b394d89d6ba63ae9">Small requests worked, so quota wasn&#x27;t exhausted. Furthermore, sending a dummy 30KB system prompt with all 36 tools also returned <b>HTTP 200 OK</b>, proving total payload size was not the trigger.</div><div class="notion-text notion-block-3b7dfb2b64f880c9960bf03f3c182eed">The failure was <b>content-dependent</b>.</div><div></div><div class="notion-text notion-block-3b7dfb2b64f880e8b475f8bf1d6e4ab3">By systematically binary-searching Hermes’ 19,000-character system prompt line by line against the live API, I isolated the exact sentence causing the 429 rejection:</div><div class="notion-text notion-block-3b7dfb2b64f8809da29eff7ccb12d23d">Testing variations of this sentence confirmed the behavior:</div><table class="notion-simple-table notion-block-3b7dfb2b64f880af937bfb47f752c80a"><tbody><tr class="notion-simple-table-row notion-block-3b7dfb2b64f880d0a5efef1a75f80cdb"><td class="" style="width:120px"><div class="notion-simple-table-cell">System Instruction Variation</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">API Result</div></td></tr><tr class="notion-simple-table-row notion-block-3b7dfb2b64f880b8aec7ca47760e0991"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">&quot;You are Hermes Agent.&quot;</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">✅ <b>200 OK</b></div></td></tr><tr class="notion-simple-table-row notion-block-3b7dfb2b64f880359e7ce7c42c4ad80a"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">&quot;You are an intelligent AI assistant created by Nous Research.&quot;</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">✅ <b>200 OK</b></div></td></tr><tr class="notion-simple-table-row notion-block-3b7dfb2b64f8804d8101dbe9cd988409"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">&quot;You run on Hermes Agent (by Nous Research).&quot;</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">✅ <b>200 OK</b></div></td></tr><tr class="notion-simple-table-row notion-block-3b7dfb2b64f880abaff4fb85f9dafc07"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">&quot;You are Hermes Agent, an intelligent AI assistant created by Nous Research&quot;</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">❌ <b>429 EXHAUSTED</b></div></td></tr><tr class="notion-simple-table-row notion-block-3b7dfb2b64f880a0bdf4ebd2165e680a"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">&quot;You are Hermes Agent, an intelligent AI assistant created by X Corp&quot;</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">✅ <b>200 OK</b></div></td></tr><tr class="notion-simple-table-row notion-block-3b7dfb2b64f8809fb7a0f5326d9680ad"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">&quot;You are a Helpful Agent, an intelligent AI assistant created by Nous Research&quot;</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">✅ <b>200 OK</b></div></td></tr></tbody></table><div></div><div class="notion-text notion-block-3b7dfb2b64f880b2a86ae2c179403a3b">Google’s Cloud Code Assist API runs an upstream classifier that filters out system instructions asserting a <b>competing assistant identity</b>. When triggered, the API returns a masked <code class="notion-inline-code">429 RESOURCE_EXHAUSTED</code> status code rather than a standard policy error.</div><div class="notion-text notion-block-3b7dfb2b64f880dd9f62e22f55a424d3"><code class="notion-inline-code">omp</code> avoids this because it injects Google&#x27;s own identity line (<em>&quot;You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team...&quot;</em>).</div><div class="notion-text notion-block-3b7dfb2b64f880169330f3e095c000d3">To fix this in Hermes without altering behavioral instructions, I added a targeted regex scrubber in <code class="notion-inline-code">transform.py</code>:</div><div class="notion-text notion-block-3b7dfb2b64f8803ab066d50508f19380">Applying this scrub restored immediate <b>HTTP 200 OK</b> responses while preserving all of Hermes&#x27; tool guidance, formatting rules, and session memory.</div><hr class="notion-hr notion-block-3b7dfb2b64f880c8bf04d49942455889"/><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f8809cad74f5a4ac8ae830" data-id="3b7dfb2b64f8809cad74f5a4ac8ae830"><span><div id="3b7dfb2b64f8809cad74f5a4ac8ae830" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8809cad74f5a4ac8ae830" title="Challenge 3: Solving Hermes&#x27; Dual Provider Resolution System"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Challenge 3: Solving Hermes&#x27; Dual Provider Resolution System</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f88032946ffa242fa9678f">With single-shot requests working (<code class="notion-inline-code">hermes -z</code>), running <code class="notion-inline-code">hermes model</code> or switching models in the TUI (<code class="notion-inline-code">/model</code>) raised:</div><div></div><div class="notion-text notion-block-3b7dfb2b64f88019adb6e1ac5ed6bdb2">Hermes maintains two distinct provider resolution paths:</div><ol start="1" class="notion-list notion-list-numbered notion-block-3b7dfb2b64f88067b354d0a908e27aee"><li><code class="notion-inline-code"><b>resolve_runtime_provider()</b></code>: Used for execution. It queries the in-memory plugin registry where <code class="notion-inline-code">register_provider_profile()</code> registers <code class="notion-inline-code">AntigravityProfile</code>.</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-3b7dfb2b64f880c78e11f7f4865927e1"><li><code class="notion-inline-code"><b>resolve_provider_full()</b></code>: Used by <code class="notion-inline-code">hermes model</code>, TUI <code class="notion-inline-code">/model</code>, and <code class="notion-inline-code">switch_model()</code>. It queries:</li><ol class="notion-list notion-list-numbered notion-block-3b7dfb2b64f880c78e11f7f4865927e1"><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88051bcd5ceb7cfd8b254"><li>Config file (<code class="notion-inline-code">providers:</code> block in <code class="notion-inline-code">config.yaml</code>)</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88003868ff67b50e8614f"><li>Built-in aliases (<code class="notion-inline-code">get_provider</code>)</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88084a516e5cfc81d8305"><li>Saved custom providers</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8804b81f5c53994a19d33"><li><code class="notion-inline-code">models.dev</code> catalog</li></ul></ol></ol><div class="notion-text notion-block-3b7dfb2b64f88095afb5e5ffaa296d45">Because <code class="notion-inline-code">resolve_provider_full()</code> does not query the runtime plugin registry, explicit provider switches to <code class="notion-inline-code">antigravity</code> failed.</div><div></div><div class="notion-text notion-block-3b7dfb2b64f8807eb650d6d693858fd9">Adding <code class="notion-inline-code">providers.antigravity</code> to <code class="notion-inline-code">config.yaml</code> bridges <code class="notion-inline-code">resolve_provider_full()</code> while leaving <code class="notion-inline-code">resolve_runtime_provider()</code> to execute via the plugin registry.</div><div class="notion-text notion-block-3b7dfb2b64f880deaf51f8f941eae15f">To ensure this works out-of-the-box for all users, I updated <code class="notion-inline-code">_select_model()</code> in <code class="notion-inline-code">antigravity_provider/hermes_plugin.py</code> to write this section automatically whenever <code class="notion-inline-code">hermes agy select</code> runs:</div><hr class="notion-hr notion-block-3b7dfb2b64f880e9b53cecdd28bb7ab7"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f8807eb232d8f9fa06250a" data-id="3b7dfb2b64f8807eb232d8f9fa06250a"><span><div id="3b7dfb2b64f8807eb232d8f9fa06250a" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8807eb232d8f9fa06250a" title="4. Supporting New Models: Gemini 3.6 Flash &amp; Wire Mapping"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">4. Supporting New Models: Gemini 3.6 Flash &amp; Wire Mapping</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f880098b95e8911c08c88b">Google periodically updates its model lineup (e.g. introducing <b>Gemini 3.6 Flash</b>). However, sending a raw wire model string of <code class="notion-inline-code">gemini-3.6-flash</code> returns <code class="notion-inline-code">404 NOT_FOUND</code>.</div><div class="notion-text notion-block-3b7dfb2b64f880ea999ad418ba7511bc">Google&#x27;s API requires an explicit reasoning effort suffix on the wire:</div><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f880b2ad8ef37825b6f226"><li><code class="notion-inline-code">gemini-3.6-flash-low</code></li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8805d87c6ef891ad3d97a"><li><code class="notion-inline-code">gemini-3.6-flash-medium</code></li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8800bb55cf45d96ba7aee"><li><code class="notion-inline-code">gemini-3.6-flash-high</code></li></ul><div class="notion-text notion-block-3b7dfb2b64f880798de6e787d95adebe">To support <code class="notion-inline-code">google-antigravity/gemini-3.6-flash</code> in Hermes, I updated <code class="notion-inline-code">models.py</code> in the plugin to translate Hermes&#x27; reasoning effort settings into Google&#x27;s wire model IDs:</div><div class="notion-text notion-block-3b7dfb2b64f88037a5bada0a111006c5">Now, selecting <code class="notion-inline-code">google-antigravity/gemini-3.6-flash</code> in Hermes automatically routes requests to the correct Google wire target based on your active reasoning effort knob.</div><hr class="notion-hr notion-block-3b7dfb2b64f8803c82cce7aa72255bb4"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f880a4ba4de6f6f58374bb" data-id="3b7dfb2b64f880a4ba4de6f6f58374bb"><span><div id="3b7dfb2b64f880a4ba4de6f6f58374bb" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f880a4ba4de6f6f58374bb" title="5. Usage Tracking &amp; Session Insights"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">5. Usage Tracking &amp; Session Insights</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f880209299d69e0391cc02">A common question when integrating custom providers is whether usage analytics are preserved.</div><div class="notion-text notion-block-3b7dfb2b64f880d580bad5d45159f6fa">Hermes logs all token usage into <code class="notion-inline-code">~/.hermes/state.db</code> under <code class="notion-inline-code">billing_provider=&#x27;antigravity&#x27;</code>:</div><div class="notion-text notion-block-3b7dfb2b64f8809bb362eba2ea9a4d01">Running <code class="notion-inline-code">hermes insights</code> aggregates these tokens alongside your paid API providers.</div><blockquote class="notion-quote notion-block-3b7dfb2b64f880438b37df0fb44ebc3e"><div>📌 <em>Note on Quota vs. Usage: </em><em><code class="notion-inline-code">omp</code></em><em> includes a custom UI meter that polls Google&#x27;s internal </em><em><code class="notion-inline-code">fetchUsage</code></em><em> API to show remaining daily allowance. Hermes logs spent tokens per session locally.</em></div></blockquote><hr class="notion-hr notion-block-3b7dfb2b64f880bebcd5cdd0e47458f8"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f880b78a6cfe44ddc291fb" data-id="3b7dfb2b64f880b78a6cfe44ddc291fb"><span><div id="3b7dfb2b64f880b78a6cfe44ddc291fb" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f880b78a6cfe44ddc291fb" title="6. Verification &amp; Working Examples"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">6. Verification &amp; Working Examples</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f880a1a4a3da8080e5abd3">Here is how to test and verify your Antigravity integration across Hermes CLI and python environments:</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f8804a87efc5480de9c628" data-id="3b7dfb2b64f8804a87efc5480de9c628"><span><div id="3b7dfb2b64f8804a87efc5480de9c628" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8804a87efc5480de9c628" title="1. Terminal Smoke Tests"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">1. Terminal Smoke Tests</span></span></h4><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f880c0b552d8885fc4f6bc" data-id="3b7dfb2b64f880c0b552d8885fc4f6bc"><span><div id="3b7dfb2b64f880c0b552d8885fc4f6bc" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f880c0b552d8885fc4f6bc" title="2. Programmatic Model Switching Test"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">2. Programmatic Model Switching Test</span></span></h4><hr class="notion-hr notion-block-3b7dfb2b64f8800f8210f9c0b081fa43"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f88021abacd1ae9dfb6209" data-id="3b7dfb2b64f88021abacd1ae9dfb6209"><span><div id="3b7dfb2b64f88021abacd1ae9dfb6209" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88021abacd1ae9dfb6209" title="7. Developer Checklist for Custom Provider Plugins"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">7. Developer Checklist for Custom Provider Plugins</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f8805080bcd0f7ccbb6624">When developing or integrating custom model provider plugins into Hermes, keep this checklist handy:</div><table class="notion-simple-table notion-block-3b7dfb2b64f880bab390ce4d0613f1bf"><tbody></tbody></table><div class="notion-text notion-block-3b7dfb2b64f88082aa30dcb20c7c2a53">Happy coding with free frontier models in Hermes! 🚀</div></main>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Reverse Engineering Antigravity Models Auth]]></title>
        <id>https://adriandevera.vercel.app/antigravity-models</id>
        <link href="https://adriandevera.vercel.app/antigravity-models"/>
        <updated>2026-08-09T16:00:00.000Z</updated>
        <summary type="html"><![CDATA[Reverse engineering wire protocols, binary-searching system prompts, unmasking a secret identity filter, and bringing Gemini 3.6 Flash to Hermes Agent.]]></summary>
        <content type="html"><![CDATA[<main class="notion light-mode notion-page notion-block-3b7dfb2b64f880a99a41ed5773eddfec"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><h2 class="notion-h notion-h1 notion-h-indent-0 notion-block-3b7dfb2b64f880818e1ccf0c9a8d1a8a" data-id="3b7dfb2b64f880818e1ccf0c9a8d1a8a"><span><div id="3b7dfb2b64f880818e1ccf0c9a8d1a8a" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f880818e1ccf0c9a8d1a8a" title="The Mystery of the 429: How a Single Sentence Broke Google’s Free AI Models in Hermes Agent"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">The Mystery of the 429: How a Single Sentence Broke Google’s Free AI Models in Hermes Agent</span></span></h2><div class="notion-text notion-block-3b7dfb2b64f8804a80c2f59e448e721b"><em>Reverse engineering wire protocols, binary-searching system prompts, unmasking a secret identity filter, and bringing Gemini 3.6 Flash to Hermes Agent.</em></div><hr class="notion-hr notion-block-3b7dfb2b64f88038a04bfa9ab2660321"/><blockquote class="notion-quote notion-block-3b7dfb2b64f8806d95d5c6388a7e730b"><div>💡 <b>TL;DR</b>: Trying to use Google’s free <b>Antigravity</b> models (Gemini 3.1 Pro, Gemini 3.5 Flash) in <b>Hermes Agent</b> kept throwing <code class="notion-inline-code">429 RESOURCE_EXHAUSTED</code> errors, even with 100% quota remaining. The culprit wasn&#x27;t rate limits or payload size—Google&#x27;s API silently rejects requests whose system prompt contains the sentence <em>&quot;You are Hermes Agent, an intelligent AI assistant created by Nous Research&quot;</em>. Neutralizing that single identity line instantly restored full access.</div><div class="notion-text notion-block-3b7dfb2b64f88042ad73e4e02192d543">In this post, I cover how I uncovered the filter, sanitized JSON Schemas, fixed Hermes&#x27; dual provider resolution system, and wired up full support for the new <b>Gemini 3.6 Flash</b> with reasoning effort mapping.</div></blockquote><hr class="notion-hr notion-block-3b7dfb2b64f8802dbb5ae3ecf9b92cf2"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f880fdb077ddb02cd7cf48" data-id="3b7dfb2b64f880fdb077ddb02cd7cf48"><span><div id="3b7dfb2b64f880fdb077ddb02cd7cf48" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f880fdb077ddb02cd7cf48" title="1. The Dream: Free Frontier Models in Hermes"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">1. The Dream: Free Frontier Models in Hermes</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f8807cbd03cb9cfab6cada">If you’ve used <b>omp (Oh My Pi)</b>, you know the magic of Google Antigravity: direct, zero-cost access to Gemini 3.1 Pro, Gemini 3.5 Flash, Gemini 3.6 Flash, Claude 4.6 via Google, and GPT-OSS 120B.</div><div class="notion-text notion-block-3b7dfb2b64f880f99732f187eac40efc">I do most of my heavy daily coding inside <b>Hermes Agent</b> (by Nous Research). Naturally, I wanted the same free model access in Hermes. Fortunately, Hermes supports third-party provider plugins via <code class="notion-inline-code">antigravity-provider</code>, an in-process bridge that translates OpenAI-formatted chat completions into Google’s Cloud Code Assist API (<code class="notion-inline-code">v1internal:streamGenerateContent</code>).</div><div class="notion-text notion-block-3b7dfb2b64f88090ae99f726ef170d58">I installed the plugin, set <code class="notion-inline-code">provider: antigravity</code>, selected <code class="notion-inline-code">google-antigravity/gemini-3.1-pro</code>, and ran a quick smoke test.</div><div class="notion-text notion-block-3b7dfb2b64f88094ba87f7b1051c9eb1">That’s when the headaches started.</div><hr class="notion-hr notion-block-3b7dfb2b64f8801984fcd37d55e8f45b"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f8806392e5e1cb329bb706" data-id="3b7dfb2b64f8806392e5e1cb329bb706"><span><div id="3b7dfb2b64f8806392e5e1cb329bb706" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8806392e5e1cb329bb706" title="2. Chapter 1: The Setup Bosses (Imports &amp; Schemas)"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">2. Chapter 1: The Setup Bosses (Imports &amp; Schemas)</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f88021a34dda4b0cf38e4a">Before hitting the main boss, I had to clear two preliminary hurdles.</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f880b3993deaca10f15bc7" data-id="3b7dfb2b64f880b3993deaca10f15bc7"><span><div id="3b7dfb2b64f880b3993deaca10f15bc7" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f880b3993deaca10f15bc7" title="Hurdle 1: The Invisible Plugin"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Hurdle 1: The Invisible Plugin</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f8800ea46bfd2a186ff536">On the first run, Hermes crashed before making a network call:</div><div class="notion-text notion-block-3b7dfb2b64f880e7ad94fe102443c432">The plugin repository used a standard Python <code class="notion-inline-code">src/</code> layout (<code class="notion-inline-code">src/antigravity_provider</code>), but Hermes&#x27; plugin loader generated an import shim that couldn&#x27;t locate the package inside the virtual environment.</div><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f880beb4bcf1ab3f784c77"><li><b>The Fix</b>: Installing the package in editable mode (<code class="notion-inline-code">pip install -e ~/.hermes/plugins/antigravity-provider</code>) permanently bound the module to Hermes’ venv, surviving any future <code class="notion-inline-code">hermes agy login</code> shim regenerations.</li></ul><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f8801b9d90f6501693951d" data-id="3b7dfb2b64f8801b9d90f6501693951d"><span><div id="3b7dfb2b64f8801b9d90f6501693951d" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8801b9d90f6501693951d" title="Hurdle 2: Schema Indigestion"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Hurdle 2: Schema Indigestion</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f8801b85ace747716aa6c6">Once the plugin loaded, requests reached Google’s Cloud Code Assist endpoints—only to instantly bounce with <code class="notion-inline-code">400 Bad Request</code> validation errors:</div><div class="notion-text notion-block-3b7dfb2b64f880ba9bbceaa04ee11165">Hermes exports rich, expressive JSON Schemas for its tools, using standard keywords like <code class="notion-inline-code">const</code>, <code class="notion-inline-code">oneOf</code>, <code class="notion-inline-code">anyOf</code>, <code class="notion-inline-code">$ref</code>, and <code class="notion-inline-code">additionalProperties</code>. Google’s Gemini function declaration parser, however, demands strict, minimal schemas.</div><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88078b358e0795b36f96e"><li><b>The Fix</b>: I added a recursive schema sanitizer in <code class="notion-inline-code">transform.py</code> to strip unsupported JSON Schema keywords before sending tool definitions upstream:</li></ul><hr class="notion-hr notion-block-3b7dfb2b64f880059cbef2cadbfb0e8b"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f88086ba9decbec358481c" data-id="3b7dfb2b64f88086ba9decbec358481c"><span><div id="3b7dfb2b64f88086ba9decbec358481c" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88086ba9decbec358481c" title="3. Chapter 2: The Ghost 429 Quota Error"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">3. Chapter 2: The Ghost 429 Quota Error</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f8806c9ea1db4a10c601bb">With schemas sanitized, requests were finally formatted correctly. But now, every single attempt failed with a brick wall:</div><div class="notion-text notion-block-3b7dfb2b64f8805791d6de7e56b52285">A standard HTTP 429 usually means one thing: <em>you&#x27;re out of quota</em>.</div><div class="notion-text notion-block-3b7dfb2b64f880568b46cbf9fa8ce08f">Except I wasn&#x27;t.</div><div class="notion-text notion-block-3b7dfb2b64f88045865ae91faa81de24">I opened <code class="notion-inline-code">omp</code> on the exact same machine, running on the exact same Google OAuth accounts, and executed prompts against Gemini 3.5 Flash without a single hitch. Both accounts showed 100% free quota remaining.</div><div class="notion-text notion-block-3b7dfb2b64f880c18fb2e0f895df6ed7">Why would <code class="notion-inline-code">omp</code> work flawlessly while Hermes failed every single time?</div><hr class="notion-hr notion-block-3b7dfb2b64f88074b86bf4765bae5ac2"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f880fc9f55cbaa344e4210" data-id="3b7dfb2b64f880fc9f55cbaa344e4210"><span><div id="3b7dfb2b64f880fc9f55cbaa344e4210" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f880fc9f55cbaa344e4210" title="4. Chapter 3: Reverse Engineering omp"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">4. Chapter 3: Reverse Engineering <code class="notion-inline-code">omp</code></span></span></h3><div class="notion-text notion-block-3b7dfb2b64f880a89f1ec115d097c9b0">To find out what <code class="notion-inline-code">omp</code> was doing differently, I dug into <code class="notion-inline-code">~/.omp/</code> and inspected its compiled binary:</div><ol start="1" class="notion-list notion-list-numbered notion-block-3b7dfb2b64f880349d09ec7849c1ffd5"><li><b>Multi-Account Rotation</b>: <code class="notion-inline-code">omp</code> maintains four Google OAuth accounts inside SQLite (<code class="notion-inline-code">~/.omp/agent/agent.db</code>), automatically rotating when one gets throttled.</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-3b7dfb2b64f8805fa897f5b4e348c0d4"><li><b>Wire Protocol Parity</b>: <code class="notion-inline-code">omp</code> talks to the exact same endpoint (<code class="notion-inline-code">https://daily-cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse</code>) using the exact same request envelope (<code class="notion-inline-code">requestType: &quot;agent&quot;</code>, <code class="notion-inline-code">userAgent: &quot;antigravity&quot;</code>, <code class="notion-inline-code">ideType: &quot;ANTIGRAVITY&quot;</code>).</li></ol><div class="notion-text notion-block-3b7dfb2b64f880308026f1296295ffbb">The wire protocol was <b>100% identical</b>.</div><div class="notion-text notion-block-3b7dfb2b64f880f99a1ecc2f401904c2">I extracted the OAuth tokens from <code class="notion-inline-code">omp</code>&#x27;s database and ran isolated test requests using Hermes&#x27; Python client:</div><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8806c8829cb4e5d5861ac"><li><b>Small Prompt</b> (<code class="notion-inline-code">&quot;Reply with exactly: OK&quot;</code>): <b>HTTP 200 OK</b> (worked on all 4 accounts across both endpoints).</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f880c8bd89c7bb2110fa6e"><li><b>Full Hermes Agent Prompt</b> (19KB system prompt + 36 tool schemas ≈ 41KB): <b>HTTP 429 RESOURCE_EXHAUSTED</b> (failed on all 4 accounts).</li></ul><div class="notion-text notion-block-3b7dfb2b64f8808aaadec968237a9843">Small prompts worked, but full agent prompts failed. That meant it <b>wasn&#x27;t</b> an account-level quota issue. It was deterministic based on the request payload itself.</div><hr class="notion-hr notion-block-3b7dfb2b64f8800098b9f21411124148"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f8803c81e7f96283087010" data-id="3b7dfb2b64f8803c81e7f96283087010"><span><div id="3b7dfb2b64f8803c81e7f96283087010" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8803c81e7f96283087010" title="5. Chapter 4: The Rabbit Hole — Bisecting the System Prompt"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">5. Chapter 4: The Rabbit Hole — Bisecting the System Prompt</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f880dd8765eca3edb09890">Was it payload size?</div><div class="notion-text notion-block-3b7dfb2b64f880569791d8554088fd4b">I generated a dummy 30KB system prompt packed with 36 heavy tool definitions and sent it. <b>HTTP 200 OK.</b></div><div class="notion-text notion-block-3b7dfb2b64f880c5abb5f784407abb93">Size wasn&#x27;t the issue. The failure was <b>content-dependent</b>.</div><div class="notion-text notion-block-3b7dfb2b64f8800f99f0e1f5117fdf18">I began binary-searching the real 19KB Hermes system prompt line by line, testing slices against the live API. Eventually, I narrowed the failure down from 19,000 characters to a single sentence:</div><div class="notion-text notion-block-3b7dfb2b64f88006ba8cdb71120443bf">If that sentence was present in the system prompt, Google&#x27;s API returned <b>HTTP 429 RESOURCE_EXHAUSTED</b>. If I modified a single word, it succeeded.</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f880c688a3cbf9694c93b9" data-id="3b7dfb2b64f880c688a3cbf9694c93b9"><span><div id="3b7dfb2b64f880c688a3cbf9694c93b9" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f880c688a3cbf9694c93b9" title="The Bisect Matrix"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">The Bisect Matrix</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f880e8a2b2e991a9aaf112">Here is what happened when I tested variations of that sentence:</div><table class="notion-simple-table notion-block-3b7dfb2b64f880588032d1c549bb4a9b"><tbody><tr class="notion-simple-table-row notion-block-3b7dfb2b64f880d0a45eea721ffc10de"><td class="" style="width:120px"><div class="notion-simple-table-cell">System Instruction Variation</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">API Result</div></td></tr><tr class="notion-simple-table-row notion-block-3b7dfb2b64f88006b4f5c622368f4bdf"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">&quot;You are Hermes Agent.&quot;</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">✅ <b>200 OK</b></div></td></tr><tr class="notion-simple-table-row notion-block-3b7dfb2b64f8807e9d1ae06a2f3306f7"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">&quot;You are an intelligent AI assistant created by Nous Research.&quot;</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">✅ <b>200 OK</b></div></td></tr><tr class="notion-simple-table-row notion-block-3b7dfb2b64f880708aa3d4d436aac91c"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">&quot;You run on Hermes Agent (by Nous Research).&quot;</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">✅ <b>200 OK</b></div></td></tr><tr class="notion-simple-table-row notion-block-3b7dfb2b64f8801b8dd4c0e773931571"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">&quot;You are Hermes Agent, an intelligent AI assistant created by Nous Research&quot;</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">❌ <b>429 EXHAUSTED</b></div></td></tr><tr class="notion-simple-table-row notion-block-3b7dfb2b64f880cab97cc7391f56e574"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">&quot;You are Hermes Agent, an intelligent AI assistant created by X Corp&quot;</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">✅ <b>200 OK</b></div></td></tr><tr class="notion-simple-table-row notion-block-3b7dfb2b64f880c39b01ee3fa38d517f"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">&quot;You are a Helpful Agent, an intelligent AI assistant created by Nous Research&quot;</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">✅ <b>200 OK</b></div></td></tr></tbody></table><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f880149f8cd158391ff765" data-id="3b7dfb2b64f880149f8cd158391ff765"><span><div id="3b7dfb2b64f880149f8cd158391ff765" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f880149f8cd158391ff765" title="The Revelation 🤯"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">The Revelation 🤯</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f880fd9563ebaa284c8fc6">Google’s Cloud Code Assist API has an upstream filter that intercepts system instructions asserting a <b>competing assistant&#x27;s identity</b>. When triggered, instead of returning a standard policy error, the API returns a masked <code class="notion-inline-code">429 RESOURCE_EXHAUSTED</code>.</div><div class="notion-text notion-block-3b7dfb2b64f880e8ab80fdf2272bc7b6"><code class="notion-inline-code">omp</code> never ran into this because it injects Google&#x27;s own identity string (<em>&quot;You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team...&quot;</em>).</div><div class="notion-text notion-block-3b7dfb2b64f880af97f2d8fffb807d6f">Hermes&#x27; prompt opens with <code class="notion-inline-code">&quot;You are Hermes Agent, an intelligent AI assistant created by Nous Research&quot;</code>—meaning <b>every single real Hermes prompt was getting caught in Google&#x27;s identity filter.</b></div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f88051bc36e27906635e13" data-id="3b7dfb2b64f88051bc36e27906635e13"><span><div id="3b7dfb2b64f88051bc36e27906635e13" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88051bc36e27906635e13" title="The Fix"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">The Fix</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f88064bc55c0e86bbfb889">I added a light regex scrubber to <code class="notion-inline-code">transform.py</code>:</div><div class="notion-text notion-block-3b7dfb2b64f880859fe4e2352c6c5d82">The moment that scrub went in, the full 41.8KB prompt returned <b>HTTP 200 OK</b>. All of Hermes’ tool guidance, memory rules, and formatting instructions remained 100% intact—only the brand identity line was sanitized.</div><hr class="notion-hr notion-block-3b7dfb2b64f880c3bf3ff234215d5142"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f880a78534da9ddd65e454" data-id="3b7dfb2b64f880a78534da9ddd65e454"><span><div id="3b7dfb2b64f880a78534da9ddd65e454" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f880a78534da9ddd65e454" title="6. Chapter 5: The Dual Provider Resolution Architecture"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">6. Chapter 5: The Dual Provider Resolution Architecture</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f8809faa83dc586d83257a">With prompts working in one-shot mode (<code class="notion-inline-code">hermes -z</code>), I tried switching models in the TUI (<code class="notion-inline-code">/model</code>) and CLI (<code class="notion-inline-code">hermes model</code>). Immediately, a new error popped up:</div><div class="notion-text notion-block-3b7dfb2b64f880db89dff90c19bb56f8">Why did <code class="notion-inline-code">-z</code> work when model switching failed?</div><div class="notion-text notion-block-3b7dfb2b64f880af956ac016063ad770">Hermes uses <b>two different provider lookup functions</b>:</div><ol start="1" class="notion-list notion-list-numbered notion-block-3b7dfb2b64f880f5840dfd95a112d56d"><li><code class="notion-inline-code"><b>resolve_runtime_provider()</b></code>: Used for execution. It queries the in-memory plugin registry where <code class="notion-inline-code">register_provider_profile()</code> registers <code class="notion-inline-code">AntigravityProfile</code>. This is why <code class="notion-inline-code">z</code> worked.</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-3b7dfb2b64f880ffabd6c15f5c8b7185"><li><code class="notion-inline-code"><b>resolve_provider_full()</b></code>: Used by <code class="notion-inline-code">hermes model</code>, <code class="notion-inline-code">/model</code> in the TUI, and <code class="notion-inline-code">switch_model()</code>. It searches:</li><ol class="notion-list notion-list-numbered notion-block-3b7dfb2b64f880ffabd6c15f5c8b7185"><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f880c7837cceb1fb54a553"><li>Config file (<code class="notion-inline-code">providers:</code> in <code class="notion-inline-code">config.yaml</code>)</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88078a188c6714a95e990"><li>Built-in provider aliases</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f880549e27c934e35d4cd0"><li>Saved custom providers</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88095847ff3e2910896d7"><li><code class="notion-inline-code">models.dev</code> catalog</li></ul></ol></ol><div class="notion-text notion-block-3b7dfb2b64f880408410c9b2826669c9">Because <code class="notion-inline-code">resolve_provider_full()</code> <b>never checks the runtime plugin registry</b>, passing <code class="notion-inline-code">explicit_provider=&quot;antigravity&quot;</code> returned <code class="notion-inline-code">None</code>, crashing the model switcher.</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f88096a2c5ea3f6eeb23e4" data-id="3b7dfb2b64f88096a2c5ea3f6eeb23e4"><span><div id="3b7dfb2b64f88096a2c5ea3f6eeb23e4" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88096a2c5ea3f6eeb23e4" title="The Fix"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">The Fix</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f8806fb8e2c1bb1b5c4603">The error message prescribed the exact fix: add <code class="notion-inline-code">antigravity</code> to <code class="notion-inline-code">~/.hermes/config.yaml</code> under <code class="notion-inline-code">providers:</code>:</div><div class="notion-text notion-block-3b7dfb2b64f880e981c9c5fe634815e0">To make this permanent for all new setups, I patched <code class="notion-inline-code">_select_model()</code> in the plugin (<code class="notion-inline-code">antigravity_provider/hermes_plugin.py</code>) to auto-populate this section whenever <code class="notion-inline-code">hermes agy select</code> runs:</div><hr class="notion-hr notion-block-3b7dfb2b64f88009a64cd5a5a758f8a9"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f88032a29ecf87336eb610" data-id="3b7dfb2b64f88032a29ecf87336eb610"><span><div id="3b7dfb2b64f88032a29ecf87336eb610" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88032a29ecf87336eb610" title="7. Chapter 6: Adding Gemini 3.6 Flash Support"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">7. Chapter 6: Adding Gemini 3.6 Flash Support</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f880698751d0379af0ee8d">With the foundation solid, I wanted to add support for <b>Gemini 3.6 Flash</b>.</div><div class="notion-text notion-block-3b7dfb2b64f8808c9dffcd41fefb11c2">However, sending a raw model ID of <code class="notion-inline-code">gemini-3.6-flash</code> returned <code class="notion-inline-code">404 NOT_FOUND</code>. Google’s Cloud Code Assist API requires a reasoning effort suffix (<code class="notion-inline-code">-low</code>, <code class="notion-inline-code">-medium</code>, or <code class="notion-inline-code">-high</code>) on the wire for 3.6 Flash:</div><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f880f39f80c8622f924234"><li><code class="notion-inline-code">gemini-3.6-flash-low</code></li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f880b4b454f2f973580196"><li><code class="notion-inline-code">gemini-3.6-flash-medium</code></li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8801fade4db31f40dbd78"><li><code class="notion-inline-code">gemini-3.6-flash-high</code></li></ul><div class="notion-text notion-block-3b7dfb2b64f880ffab58d1debac93ed9">I updated the plugin’s <code class="notion-inline-code">models.py</code> to declare <code class="notion-inline-code">google-antigravity/gemini-3.6-flash</code> in <code class="notion-inline-code">KNOWN_MODELS</code> and map Hermes’ reasoning effort knob to the correct wire model ID:</div><div class="notion-text notion-block-3b7dfb2b64f8804babdcea11c1c2d246">Now, selecting <code class="notion-inline-code">google-antigravity/gemini-3.6-flash</code> dynamically maps reasoning effort levels (<code class="notion-inline-code">low</code>, <code class="notion-inline-code">medium</code>, <code class="notion-inline-code">high</code>) directly to Google&#x27;s expected wire model identifiers.</div><hr class="notion-hr notion-block-3b7dfb2b64f88037b824fd2f16898046"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f88093bf32c618e0c22341" data-id="3b7dfb2b64f88093bf32c618e0c22341"><span><div id="3b7dfb2b64f88093bf32c618e0c22341" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88093bf32c618e0c22341" title="8. Chapter 7: Usage &amp; Analytics"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">8. Chapter 7: Usage &amp; Analytics</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f880f98e02e21129f381c6">Does Hermes track usage for Antigravity models?</div><div class="notion-text notion-block-3b7dfb2b64f880a5b639fa2ce5b2a60c"><b>Yes!</b> Hermes logs all token usage into <code class="notion-inline-code">~/.hermes/state.db</code> under <code class="notion-inline-code">billing_provider=&#x27;antigravity&#x27;</code>:</div><div class="notion-text notion-block-3b7dfb2b64f8803ea82bd3da83ecd648">Running <code class="notion-inline-code">hermes insights</code> cleanly aggregates these models alongside your paid providers.</div><blockquote class="notion-quote notion-block-3b7dfb2b64f880268f8ce267de42ffda"><div>📌 <em>Note: The only difference between </em><em><code class="notion-inline-code">omp</code></em><em> and Hermes here is that </em><em><code class="notion-inline-code">omp</code></em><em> queries Google&#x27;s internal </em><em><code class="notion-inline-code">fetchUsage</code></em><em> API to render a live &quot;remaining daily quota&quot; meter. Hermes logs spent tokens per session locally.</em></div></blockquote><hr class="notion-hr notion-block-3b7dfb2b64f880c99386d26eb018157e"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f8808ea4f3c5cae0e0b164" data-id="3b7dfb2b64f8808ea4f3c5cae0e0b164"><span><div id="3b7dfb2b64f8808ea4f3c5cae0e0b164" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8808ea4f3c5cae0e0b164" title="9. Chapter 8: The Moment of Truth"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">9. Chapter 8: The Moment of Truth</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f88042a95ed5a504120447">With all fixes and features in place, I ran full end-to-end verification:</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f88061b37ae09650a6b213" data-id="3b7dfb2b64f88061b37ae09650a6b213"><span><div id="3b7dfb2b64f88061b37ae09650a6b213" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88061b37ae09650a6b213" title="1. Terminal Smoke Tests"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">1. Terminal Smoke Tests</span></span></h4><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f8804c8640f0e58a9c04c8" data-id="3b7dfb2b64f8804c8640f0e58a9c04c8"><span><div id="3b7dfb2b64f8804c8640f0e58a9c04c8" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8804c8640f0e58a9c04c8" title="2. Programmatic Model Switching Test"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">2. Programmatic Model Switching Test</span></span></h4></main>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[On-Site Face Verification & Multi-Tenant Payroll System]]></title>
        <id>https://adriandevera.vercel.app/nunez-dental-clinic-payroll-system</id>
        <link href="https://adriandevera.vercel.app/nunez-dental-clinic-payroll-system"/>
        <updated>2026-06-23T16:00:00.000Z</updated>
        <summary type="html"><![CDATA[Full-stack biometric attendance kiosk and multi-tenant payroll system built with Next.js 15, Flutter (on-device MobileFaceNet TFLite face recognition), Supabase Postgres, and Drizzle ORM.]]></summary>
        <content type="html"><![CDATA[<main class="notion light-mode notion-page notion-block-389dfb2b64f88176b66bd896e3ee66c2"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><h2 class="notion-h notion-h1 notion-h-indent-0 notion-block-3b7dfb2b64f88111a9bbc97f6d0a7b75" data-id="3b7dfb2b64f88111a9bbc97f6d0a7b75"><span><div id="3b7dfb2b64f88111a9bbc97f6d0a7b75" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88111a9bbc97f6d0a7b75" title="On-Site Face Verification &amp; Multi-Tenant Payroll System"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">On-Site Face Verification &amp; Multi-Tenant Payroll System</span></span></h2><div class="notion-text notion-block-3b7dfb2b64f881179b9cf98d772eeb80">A full-stack, enterprise-grade payroll and biometric attendance solution built with Next.js 15, Supabase (Postgres), Drizzle ORM, and Flutter.</div><div class="notion-text notion-block-3b7dfb2b64f88137a9baec77e05cd8e3">The system combines an on-device biometric mobile kiosk app with a multi-tenant web administration portal.</div><hr class="notion-hr notion-block-3b7dfb2b64f8816aa559de765b820237"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f88154a644fac25dddfdf1" data-id="3b7dfb2b64f88154a644fac25dddfdf1"><span><div id="3b7dfb2b64f88154a644fac25dddfdf1" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88154a644fac25dddfdf1" title="1. Problem &amp; Challenge"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">1. Problem &amp; Challenge</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f8816eb4a2f21716949425">Traditional payroll systems rely on manual timecards, badge swipes, or web-based timekeeping prone to buddy punching (employees clocking in for one another) and location fraud.</div><div class="notion-text notion-block-3b7dfb2b64f881e8a7a5f57709d1c030">Client companies needed:</div><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8816aa7b9fd21901774bd"><li>Zero-Trust Attendance Verification: Fraud-proof biometric verification directly at physical work sites.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881f4b7e5fbd4e9226e36"><li>Offline-First Kiosk Reliability: Ability to clock in/out during network drops or remote site shifts.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8813d9827d963c8aa0581"><li>Multi-Tenant Isolation: Complete data isolation between client companies from a single deployment.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881c9b5b7cf4c4ec2c0e5"><li>Compliant Philippine Payroll Engine: Automated calculations for daily/monthly rates, overtime rates, and statutory contributions (SSS, PhilHealth, Pag-IBIG).</li></ul><hr class="notion-hr notion-block-3b7dfb2b64f881618965c8972d3f94ba"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f881f9a5c1d6334b3228cb" data-id="3b7dfb2b64f881f9a5c1d6334b3228cb"><span><div id="3b7dfb2b64f881f9a5c1d6334b3228cb" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881f9a5c1d6334b3228cb" title="2. System Architecture"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">2. System Architecture</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f88169876bca8516edf7d7">The solution consists of two primary components communicating with a centralized Supabase/Postgres backend:</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f8813084a4d6751ef71a1b" data-id="3b7dfb2b64f8813084a4d6751ef71a1b"><span><div id="3b7dfb2b64f8813084a4d6751ef71a1b" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8813084a4d6751ef71a1b" title="A. Companion Mobile Kiosk App (`/mobile_app` — Flutter)"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">A. Companion Mobile Kiosk App (`/mobile_app` — Flutter)</span></span></h4><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881da9c55f8383805c154"><li>**On-Device Biometrics:** Powered by Google ML Kit (face detection) and MobileFaceNet (TFLite) for local 192-dimensional vector embedding generation.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881ae90a4cee0833dc39e"><li>**Privacy-First Face Matching:** Facial recognition runs entirely on-device; biometric vector embeddings are never sent to external third-party cloud AI APIs.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88145bc42ed52a67fb8d2"><li>**Biometric Liveness Verification:** Multi-angle head pose detection (blinking, smiling, turning) to prevent static photo or video playback spoofs.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881a9a784ca0bc6ea276b"><li>**Network &amp; Location Guard:** Enforces clock-ins only when connected to verified site Wi-Fi SSIDs or within geofenced GPS boundaries.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88156bab3d06ca39094a1"><li>**Offline Sync &amp; Reconciliation:** Stores encrypted attendance logs locally during connectivity outages and reconciles automatically when connection restores.</li></ul><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f881baaa45ccb238be1191" data-id="3b7dfb2b64f881baaa45ccb238be1191"><span><div id="3b7dfb2b64f881baaa45ccb238be1191" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881baaa45ccb238be1191" title="B. HR &amp; Payroll Admin Dashboard (`/payroll-system` — Next.js 15)"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">B. HR &amp; Payroll Admin Dashboard (`/payroll-system` — Next.js 15)</span></span></h4><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881c6b8fcf8b2cdc46498"><li>**Multi-Tenant Portal:** Company member roles, location management, and kiosk device pairing via secure pairing codes.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8812a8e19f1c4f1961ea3"><li>**Payroll Processing Engine:** Real-time summary of regular hours, overtime, tardiness, and statutory contribution tiers.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88141b3cfd24ffe99c7e6"><li>**Audit &amp; Evidence Viewer:** Admin review of attendance scan events with captured verification thumbnails and network metadata.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8817b9bd3f6e268df65b2"><li>**Employee Portal:** Self-service payslip viewer and attendance history.</li></ul><hr class="notion-hr notion-block-3b7dfb2b64f88111b583e4b540be76dc"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f8817fbe19c3d7cbdb6b40" data-id="3b7dfb2b64f8817fbe19c3d7cbdb6b40"><span><div id="3b7dfb2b64f8817fbe19c3d7cbdb6b40" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8817fbe19c3d7cbdb6b40" title="3. Technical Highlights"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">3. Technical Highlights</span></span></h3><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f8817f83b2ff61aafc2507" data-id="3b7dfb2b64f8817f83b2ff61aafc2507"><span><div id="3b7dfb2b64f8817f83b2ff61aafc2507" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8817f83b2ff61aafc2507" title="1. High-Performance On-Device Face Recognition"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">1. High-Performance On-Device Face Recognition</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f88149b9b5d5f86a18c52f">By running MobileFaceNet inference locally via TFLite on the Flutter app, biometric verification finishes in &lt;300ms without server roundtrips. Facial embeddings are compared using cosine similarity against locally cached employee face profiles.</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f881259ef2eb0b50f10d32" data-id="3b7dfb2b64f881259ef2eb0b50f10d32"><span><div id="3b7dfb2b64f881259ef2eb0b50f10d32" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881259ef2eb0b50f10d32" title="2. Multi-Tenant Architecture &amp; Data Security"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">2. Multi-Tenant Architecture &amp; Data Security</span></span></h4><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8816d8fe3dc93ee54d51c"><li>**Row Level Security (RLS):** Enforces strict company boundaries across Postgres tables (`companies`, `employees`, `attendance_logs`, `kiosk_devices`).</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881e1b146fd3a6aae1e8d"><li>**Secure Kiosk Provisioning:** Kiosk devices pair with company accounts via short-lived alphanumeric pairing codes and device secret tokens.</li></ul><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f881b496c5e443324686bd" data-id="3b7dfb2b64f881b496c5e443324686bd"><span><div id="3b7dfb2b64f881b496c5e443324686bd" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881b496c5e443324686bd" title="3. Automated Statutory Computation Engine"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">3. Automated Statutory Computation Engine</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f8819a94c2f3fe4289fc41">Payroll runs automatically compute:</div><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88113bcc3fdec7a9dea68"><li>Gross Pay &amp; Overtime Premiums (Regular Overtime, Rest Day, Special Non-Working, and Holiday rates).</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8813c9fecc8d17924d8f8"><li>Philippine Statutory Deductions: Tiered SSS bracket lookup, percentage-based PhilHealth with salary caps, and Pag-IBIG mandatory contributions.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8814a9ffde2fc18528360"><li>Net Pay Calculation &amp; PDF Payslip Generation.</li></ul><hr class="notion-hr notion-block-3b7dfb2b64f881e2be6aceef6b6aa37c"/><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f88100a058c5bccea44e42" data-id="3b7dfb2b64f88100a058c5bccea44e42"><span><div id="3b7dfb2b64f88100a058c5bccea44e42" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88100a058c5bccea44e42" title="4. Tech Stack Overview"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">4. Tech Stack Overview</span></span></h3><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8810eb607eb5b969546c2"><li>**Mobile Kiosk:** Flutter, Dart, Google ML Kit, MobileFaceNet (TFLite), SQLite, Supabase Flutter SDK</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88163a7c3f73d7e7e3e0f"><li>**Web Portal:** Next.js 15 (App Router), TypeScript, Tailwind CSS 4, DaisyUI 5, Drizzle ORM</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8817fa178e4f64daa7942"><li>**Database &amp; Auth:** Supabase PostgreSQL, Supabase Auth, Row Level Security (RLS)</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881b4a708e38d691c1a9d"><li>**Deployment:** Vercel (Web Dashboard), Docker (Self-Hosted Postgres / Container Host)</li></ul><hr class="notion-hr notion-block-3b7dfb2b64f881359e3fd1098d003880"/><div class="notion-text notion-block-3b7dfb2b64f8811395e8f9cc0add6b3f">*(Note: Proprietary enterprise platform. Specific client credentials, internal API keys, and database connections are excluded for privacy.)*</div></main>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[Dental Laboratory Management System for Nunez Dental Clinic]]></title>
        <id>https://adriandevera.vercel.app/nunez-dental-lab-management-system</id>
        <link href="https://adriandevera.vercel.app/nunez-dental-lab-management-system"/>
        <updated>2026-06-23T16:00:00.000Z</updated>
        <summary type="html"><![CDATA[A real-time lab job tracking and inventory system for a dental clinic — managing case pipelines, technician handoffs, and supply stock levels.]]></summary>
        <content type="html"><![CDATA[<main class="notion light-mode notion-page notion-block-389dfb2b64f881999081d3927333fcca"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-3b7dfb2b64f8814f916bc0dc6c75b5bb" data-id="3b7dfb2b64f8814f916bc0dc6c75b5bb"><span><div id="3b7dfb2b64f8814f916bc0dc6c75b5bb" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8814f916bc0dc6c75b5bb" title="The Problem"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">The Problem</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f881afbba9c2cce510a55e">Dental laboratories handle dozens of active cases simultaneously — crowns, bridges, dentures, and retainers. Each item moves through multiple manufacturing stages, assigned to different technicians, and delivered back to specific dentists. Managing this with paper logs or sticky notes quickly leads to lost jobs and missed delivery dates.</div><div class="notion-text notion-block-3b7dfb2b64f881b9ab73e2c2fd5f0c64">Nunez Dental Clinic needed a central hub to manage lab cases end-to-end.</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-3b7dfb2b64f881798e98dbcddbef168e" data-id="3b7dfb2b64f881798e98dbcddbef168e"><span><div id="3b7dfb2b64f881798e98dbcddbef168e" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881798e98dbcddbef168e" title="System Capabilities"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">System Capabilities</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f881f69715cf0ecf70e33c">The Dental Laboratory Management System provides full operational visibility across the lab floor:</div><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881b08686f3bf6c0d5629"><li>Case Tracking: Log incoming jobs with patient details, case types, attending dentist, shade specifications, and target due dates.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88121aee5c9b2e2d3f1cf"><li>Status Pipeline: Visual Kanban stages (Received -&gt; In Progress -&gt; Quality Check -&gt; Ready -&gt; Delivered).</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881dd834de111e292a124"><li>Material &amp; Inventory Tracking: Log materials consumed per case to track stock levels and cost of goods sold.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881c9a369e190e7431681"><li>Delivery &amp; Pickup Logs: Record timestamps and courier signatures upon pickup or delivery.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8815da0add503ca0db8bb"><li>Doctor / Client Portal: View pending cases per doctor and track status without calling the lab.</li></ul><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-3b7dfb2b64f881b3b26efbb9d5e6f0a8" data-id="3b7dfb2b64f881b3b26efbb9d5e6f0a8"><span><div id="3b7dfb2b64f881b3b26efbb9d5e6f0a8" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881b3b26efbb9d5e6f0a8" title="Technical Architecture"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Technical Architecture</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f8814fbe51e2347b43dc62">Built with Next.js, TypeScript, and Supabase.</div><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881deb5b3fcc6bf892d93"><li>Real-Time Synchronization: Supabase real-time subscriptions keep the lab floor and front desk synchronized. When a technician updates a case stage on a tablet, the receptionist sees it immediately.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88135887dfc23d6ec5c91"><li>Optimistic UI Updates: UI state updates instantly while requests settle in the background, keeping the floor workflow fast for lab technicians handling tools.</li></ul><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-3b7dfb2b64f881858a7de82b8c434dc8" data-id="3b7dfb2b64f881858a7de82b8c434dc8"><span><div id="3b7dfb2b64f881858a7de82b8c434dc8" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881858a7de82b8c434dc8" title="Key Design Decisions"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Key Design Decisions</span></span></h3><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-3b7dfb2b64f881978531f430b5054e95" data-id="3b7dfb2b64f881978531f430b5054e95"><span><div id="3b7dfb2b64f881978531f430b5054e95" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881978531f430b5054e95" title="Stage Transition Rules"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Stage Transition Rules</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f881d6ada3f914132b8537">To ensure cases do not skip quality assurance steps, state transitions enforce prerequisite checks (e.g., a case cannot move to Ready until all material consumption logs and QA sign-offs are submitted).</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-3b7dfb2b64f881b9b8e5e3f90f33cd4a" data-id="3b7dfb2b64f881b9b8e5e3f90f33cd4a"><span><div id="3b7dfb2b64f881b9b8e5e3f90f33cd4a" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881b9b8e5e3f90f33cd4a" title="Inventory Deductions"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Inventory Deductions</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f8816e8abfe2ae1a0ac555">Material usage is linked directly to case completion, automatically decrementing physical stock (porcelain blocks, acrylics, metal alloys) and triggering reorder alerts when thresholds are reached.</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-3b7dfb2b64f881fbb353dc61c4e7defb" data-id="3b7dfb2b64f881fbb353dc61c4e7defb"><span><div id="3b7dfb2b64f881fbb353dc61c4e7defb" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881fbb353dc61c4e7defb" title="Outcome &amp; Impact"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Outcome &amp; Impact</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f881f3b625c95f0e15659f">The lab team replaced physical paper slips with a single live dashboard. Turnaround tracking helps them spot bottlenecks on the floor before deadlines slip, improving on-time delivery to partnering dentists.</div><div class="notion-text notion-block-3b7dfb2b64f881678042d6377a8940bd">*(Note: This is a private client project built for internal clinic and lab operations.)*</div></main>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[ISCP ID Generator — Letting Students Build Their Own School IDs]]></title>
        <id>https://adriandevera.vercel.app/iscp-id-generator</id>
        <link href="https://adriandevera.vercel.app/iscp-id-generator"/>
        <updated>2026-06-23T16:00:00.000Z</updated>
        <summary type="html"><![CDATA[A web app for ISCP students to generate high-resolution print-ready student ID cards directly in the browser via Google OAuth and Firestore.]]></summary>
        <content type="html"><![CDATA[<main class="notion light-mode notion-page notion-block-389dfb2b64f881f1ae39d48e512ec1f6"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-3b7dfb2b64f881bebb6deddeb1a0f19d" data-id="3b7dfb2b64f881bebb6deddeb1a0f19d"><span><div id="3b7dfb2b64f881bebb6deddeb1a0f19d" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881bebb6deddeb1a0f19d" title="The Idea"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">The Idea</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f881df9ab2e7dfe485319f">Getting an official school ID at the International State College of the Philippines was often a slow process involving manual forms and long wait times. I built the ISCP ID Generator so students could log in, enter their details, and generate a print-ready school ID card directly in their browser.</div><div class="notion-text notion-block-3b7dfb2b64f881fb948dd2f9bcd78aeb">No graphic design tools required, and no waiting in line.</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-3b7dfb2b64f8811282dce20e30c17857" data-id="3b7dfb2b64f8811282dce20e30c17857"><span><div id="3b7dfb2b64f8811282dce20e30c17857" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8811282dce20e30c17857" title="System Overview &amp; Workflow"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">System Overview &amp; Workflow</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f8817d9e79f9b6aa04532f">1. Student Auth: Sign in with Google OAuth via Firebase Authentication.</div><div class="notion-text notion-block-3b7dfb2b64f8810391bcf8e23917f1c2">2. Information Input: Enter student details (Full Name, Course, Year Level, Student Number, Emergency Contact).</div><div class="notion-text notion-block-3b7dfb2b64f88112956acdfda13d4207">3. Client-Side Rendering: Live interactive preview of the ID card with customizable template themes and photo uploads.</div><div class="notion-text notion-block-3b7dfb2b64f881b59f7ded7f604ed599">4. Export Options: Instant browser rendering to high-resolution PNG or print-ready PDF.</div><div class="notion-text notion-block-3b7dfb2b64f88112acfbee16052627a5">5. Persistent Profile: Student data is saved in Firestore so returning users can update details or regenerate lost cards without starting over.</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-3b7dfb2b64f881b5b44fe32d4e2d9d26" data-id="3b7dfb2b64f881b5b44fe32d4e2d9d26"><span><div id="3b7dfb2b64f881b5b44fe32d4e2d9d26" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881b5b44fe32d4e2d9d26" title="Tech Stack"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Tech Stack</span></span></h3><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8815ba427e5d715f6d272"><li>Framework: Next.js 13 (App Router)</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881438e51f92be7b08293"><li>Language: TypeScript</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88105b53fee299edc1f52"><li>Auth &amp; Database: Firebase (Authentication + Firestore)</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881f380f5d1bf1b82e805"><li>Styling: Tailwind CSS</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88140a471f7c37fa5f137"><li>Deployment &amp; CI/CD: Vercel + GitHub Actions</li></ul><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-3b7dfb2b64f881358e8ced61c06f18ad" data-id="3b7dfb2b64f881358e8ced61c06f18ad"><span><div id="3b7dfb2b64f881358e8ced61c06f18ad" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881358e8ced61c06f18ad" title="Technical Highlights &amp; Lessons Learned"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Technical Highlights &amp; Lessons Learned</span></span></h3><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-3b7dfb2b64f881e1a2ccc60027b7196d" data-id="3b7dfb2b64f881e1a2ccc60027b7196d"><span><div id="3b7dfb2b64f881e1a2ccc60027b7196d" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881e1a2ccc60027b7196d" title="1. Canvas vs DOM Rendering"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">1. Canvas vs DOM Rendering</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f881448099cb0ce590e077">To render high-DPI printable ID cards, I experimented with HTML5 Canvas vs DOM-to-Image libraries (`html2canvas` / `dom-to-image`). DOM-to-Image with SVG filters proved best for maintaining sharp typography across different printer resolutions.</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-3b7dfb2b64f88149a3a4f07316a2737b" data-id="3b7dfb2b64f88149a3a4f07316a2737b"><span><div id="3b7dfb2b64f88149a3a4f07316a2737b" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88149a3a4f07316a2737b" title="2. Next.js App Router with Firebase"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">2. Next.js App Router with Firebase</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f881229830de055b4f8ebb">This project served as an early deep dive into the Next.js App Router architecture. I learned how to manage Firebase client SDK instances across client components while keeping API routes clean.</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-3b7dfb2b64f8810e9e08d8ba60f47187" data-id="3b7dfb2b64f8810e9e08d8ba60f47187"><span><div id="3b7dfb2b64f8810e9e08d8ba60f47187" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8810e9e08d8ba60f47187" title="Source Code"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Source Code</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f88163bd08d8586989654f">The project is open source and available on GitHub: github.com/Adrian-stuff/iscp-id-generator</div></main>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[ThinkLog - A centralized hub for developer article]]></title>
        <id>https://adriandevera.vercel.app/thinklog</id>
        <link href="https://adriandevera.vercel.app/thinklog"/>
        <updated>2026-05-18T16:00:00.000Z</updated>
        <summary type="html"><![CDATA[A developer article aggregator built with Node.js, Express, Supabase Postgres, and Alpine.js — featuring real-time RSS extraction and full-text search.]]></summary>
        <content type="html"><![CDATA[<main class="notion light-mode notion-page notion-block-365dfb2b64f8807ba8b3e1aef69d46f3"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><h2 class="notion-h notion-h1 notion-h-indent-0 notion-block-3b7dfb2b64f88181b701e00df62813f7" data-id="3b7dfb2b64f88181b701e00df62813f7"><span><div id="3b7dfb2b64f88181b701e00df62813f7" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88181b701e00df62813f7" title="Building ThinkLog — A Centralized Hub for Developer Articles"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Building ThinkLog — A Centralized Hub for Developer Articles</span></span></h2><div class="notion-text notion-block-3b7dfb2b64f8815cae03c7bf51f43684">ThinkLog is a full-stack web application that aggregates articles from engineering blogs, community platforms, and developer sites into a single fast stream. Instead of hopping between a dozen bookmarks, users get a clean feed with built-in full-text search, syntax-highlighted code blocks, and inline discussions.</div><div class="notion-text notion-block-3b7dfb2b64f881d6b95ae2defec93ffb">Built for Web Systems &amp; Technologies and Quantitative Methods at Pamantasan ng Lungsod ng Valenzuela, ThinkLog demonstrates modern full-stack development using a lightweight, performant frontend architecture.</div><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f881a6b5a0c7446e67419c" data-id="3b7dfb2b64f881a6b5a0c7446e67419c"><span><div id="3b7dfb2b64f881a6b5a0c7446e67419c" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881a6b5a0c7446e67419c" title="Tech Stack &amp; Architecture"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Tech Stack &amp; Architecture</span></span></h3><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881279ab2ee25a23a78e1"><li>Frontend: Alpine.js, Tailwind CSS (CDN), Highlight.js</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8817f9be6c27e974fc3f2"><li>Backend: Node.js (Express)</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881ad846bf7c027ca4ee2"><li>Database: PostgreSQL (via Supabase)</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8813e8f6dd0a469c094dc"><li>Authentication: Supabase Auth (Email/Password, Google OAuth, GitHub OAuth)</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8813bbad3d253cc85abf6"><li>RSS Processing: Custom RSS parser with `@extractus/article-extractor`</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88144afe7f849826f0794"><li>Hosting: Vercel</li></ul><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f881fb8f04f88f7ffeb56d" data-id="3b7dfb2b64f881fb8f04f88f7ffeb56d"><span><div id="3b7dfb2b64f881fb8f04f88f7ffeb56d" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881fb8f04f88f7ffeb56d" title="Key Features"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Key Features</span></span></h3><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f8813e997dcc78aeb96458" data-id="3b7dfb2b64f8813e997dcc78aeb96458"><span><div id="3b7dfb2b64f8813e997dcc78aeb96458" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8813e997dcc78aeb96458" title="Aggregated Feed &amp; Filtering"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Aggregated Feed &amp; Filtering</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f8819fb04dd20689d304ad">Pulls articles from multiple RSS sources into a responsive card layout. Users can filter by source publication and sort by Recommended, Latest, Popular, Trending, or Discover.</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f881ef830fc7c5e91a86f8" data-id="3b7dfb2b64f881ef830fc7c5e91a86f8"><span><div id="3b7dfb2b64f881ef830fc7c5e91a86f8" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881ef830fc7c5e91a86f8" title="Full-Featured Article Reader"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Full-Featured Article Reader</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f8811788abf5371f8aa20a">Clicking any article opens a dedicated reader view with parsed article body text, syntax-highlighted code blocks with one-click copy, author metadata, and original source links.</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f8812c8c57e17d1e45cfcb" data-id="3b7dfb2b64f8812c8c57e17d1e45cfcb"><span><div id="3b7dfb2b64f8812c8c57e17d1e45cfcb" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8812c8c57e17d1e45cfcb" title="Community &amp; Social Features"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Community &amp; Social Features</span></span></h4><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881a6a933f90b89aa789d"><li>Reactions: Like, Love, or Insightful reactions per post.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881cb8e46f92cfef8ed70"><li>Inline Comments: Discussion threads on feed cards and full reader views.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88158a5caef77c46abeda"><li>Native Sharing: Web Share API integration with automatic fallback to clipboard copy.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881edb8a1dd581a1155bb"><li>Live Search: PostgreSQL full-text search with instant autocomplete suggestions.</li></ul><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f88147901ee7ff15f43dc5" data-id="3b7dfb2b64f88147901ee7ff15f43dc5"><span><div id="3b7dfb2b64f88147901ee7ff15f43dc5" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88147901ee7ff15f43dc5" title="Engineering &amp; Architecture Decisions"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Engineering &amp; Architecture Decisions</span></span></h3><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f8810fa4b4f02f49440cf5" data-id="3b7dfb2b64f8810fa4b4f02f49440cf5"><span><div id="3b7dfb2b64f8810fa4b4f02f49440cf5" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8810fa4b4f02f49440cf5" title="Single HTML File SPA with Alpine.js"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Single HTML File SPA with Alpine.js</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f88103a5a2f76bc187a58d">The entire frontend is built as a single `index.html` file using Alpine.js for state management and view transitions. No heavy build tools, bundlers, or framework overhead — just lightweight CDN-served reactivity.</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f88105a6a0fb0a323f1959" data-id="3b7dfb2b64f88105a6a0fb0a323f1959"><span><div id="3b7dfb2b64f88105a6a0fb0a323f1959" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88105a6a0fb0a323f1959" title="Eliminating N+1 Queries via Postgres JSON Aggregation"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Eliminating N+1 Queries via Postgres JSON Aggregation</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f8811aa57ac04059bce676">Rather than firing multiple database queries for posts, authors, tags, and comments, the Express backend utilizes PostgreSQL `json_build_object` and `json_agg` functions. A single database query returns fully formatted JSON payloads ready for client rendering.</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f881f6bb0ef092d906294b" data-id="3b7dfb2b64f881f6bb0ef092d906294b"><span><div id="3b7dfb2b64f881f6bb0ef092d906294b" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881f6bb0ef092d906294b" title="Resilient Client-Side Routing"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Resilient Client-Side Routing</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f8819cae9fd5eff98bcbe8">Static hosting on Vercel uses a catch-all rewrite rule to route all traffic to `index.html`. Alpine.js paired with the HTML5 History API manages path changes seamlessly without triggering full page reloads.</div><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f8816abc66f6a188b31f21" data-id="3b7dfb2b64f8816abc66f6a188b31f21"><span><div id="3b7dfb2b64f8816abc66f6a188b31f21" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8816abc66f6a188b31f21" title="Database Schema Overview"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Database Schema Overview</span></span></h3><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f881a29e46f0ae31939e02" data-id="3b7dfb2b64f881a29e46f0ae31939e02"><span><div id="3b7dfb2b64f881a29e46f0ae31939e02" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881a29e46f0ae31939e02" title="Takeaways &amp; Lessons Learned"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Takeaways &amp; Lessons Learned</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f881448d0be6a29823483d">1. Lightweight SPAs: Alpine.js excels at building interactive dashboards without complex build chains.</div><div class="notion-text notion-block-3b7dfb2b64f8811da419e7a63fee4cca">2. Server-Side Data Bundling: Database-level JSON aggregation significantly reduces network roundtrips and API latency.</div><div class="notion-text notion-block-3b7dfb2b64f8814c940bee7608d257e8">3. Content Extraction: Parsing third-party RSS feeds requires robust HTML sanitization to prevent XSS while preserving formatting.</div><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f881c294cfdbfdb6d0e6e4" data-id="3b7dfb2b64f881c294cfdbfdb6d0e6e4"><span><div id="3b7dfb2b64f881c294cfdbfdb6d0e6e4" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881c294cfdbfdb6d0e6e4" title="Live Demo &amp; Code"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Live Demo &amp; Code</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f8811b99bacd6b16268c5d">Live Site: thinklogapp.vercel.app | Source Code: github.com/Adrian-stuff/thinklog</div></main>]]></content>
    </entry>
    <entry>
        <title type="html"><![CDATA[HOACentralPH — Multi-tenant SaaS Platform]]></title>
        <id>https://adriandevera.vercel.app/hoacentralph-project</id>
        <link href="https://adriandevera.vercel.app/hoacentralph-project"/>
        <updated>2025-11-18T16:00:00.000Z</updated>
        <summary type="html"><![CDATA[A multi-tenant SaaS platform for Homeowners Associations in the Philippines built with Next.js App Router, TypeScript, Tailwind, and Supabase.]]></summary>
        <content type="html"><![CDATA[<main class="notion light-mode notion-page notion-block-8d4f9970bba34683835b88a63907dd46"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><h2 class="notion-h notion-h1 notion-h-indent-0 notion-block-3b7dfb2b64f881779f5ee21922355d9c" data-id="3b7dfb2b64f881779f5ee21922355d9c"><span><div id="3b7dfb2b64f881779f5ee21922355d9c" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881779f5ee21922355d9c" title="HOACentralPH — Multi-tenant SaaS Platform"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">HOACentralPH — Multi-tenant SaaS Platform</span></span></h2><div class="notion-text notion-block-3b7dfb2b64f88119a384f2d39e0d0381">HOACentralPH is a SaaS platform designed for Homeowners Associations (HOAs) in the Philippines. It features a multi-tenant architecture that isolates organization data while serving multiple subdivisions from a single unified codebase.</div><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f881dd9457fdafa206e261" data-id="3b7dfb2b64f881dd9457fdafa206e261"><span><div id="3b7dfb2b64f881dd9457fdafa206e261" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881dd9457fdafa206e261" title="Product Overview &amp; Problem Statement"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Product Overview &amp; Problem Statement</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f881f2aaccd9475e2fe85c">Many residential subdivisions and HOAs in the Philippines rely on manual record-keeping, paper receipts for monthly dues, and fragmented chat groups for official announcements. This causes bookkeeping errors, poor payment visibility, and compliance friction.</div><div class="notion-text notion-block-3b7dfb2b64f8818fba15e4b4377bb132">HOACentralPH provides a centralized portal for HOA officers, board members, and homeowners to manage operations, track dues, handle facility reservations, and issue official notices.</div><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f8815981edd0224819b54b" data-id="3b7dfb2b64f8815981edd0224819b54b"><span><div id="3b7dfb2b64f8815981edd0224819b54b" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8815981edd0224819b54b" title="Core Features"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Core Features</span></span></h3><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f881f799aece151008785f" data-id="3b7dfb2b64f881f799aece151008785f"><span><div id="3b7dfb2b64f881f799aece151008785f" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881f799aece151008785f" title="Multi-Tenant Architecture &amp; Subdomains"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Multi-Tenant Architecture &amp; Subdomains</span></span></h4><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8814c867fef5391ba9834"><li>Custom Subdomains: Each HOA organization gets its own dedicated vanity domain/subdomain (e.g., `greenwood.hoacentral.ph`).</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881718481d181af867a8c"><li>Subdivision Landing Pages: Public-facing marketing and information page per HOA.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8819cbb6ee54df07df715"><li>Isolated Tenant Data: Strict tenant query boundaries ensure no cross-subdivision data leaks.</li></ul><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f8819087c8c3594f3da34b" data-id="3b7dfb2b64f8819087c8c3594f3da34b"><span><div id="3b7dfb2b64f8819087c8c3594f3da34b" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f8819087c8c3594f3da34b" title="Financial Management &amp; Dues Tracking"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Financial Management &amp; Dues Tracking</span></span></h4><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881dbb6a1dd667bea4252"><li>Automated Invoicing: Generate monthly association dues statements automatically.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881838bf0ca0d44f0f813"><li>Payment Processing &amp; Verification: Homeowners submit proof of payment; admins verify and issue digital receipts.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881daae73eef141216bf9"><li>Expense &amp; Budget Reporting: Real-time financial ledger tracking income vs. operational expenses.</li></ul><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f881a1b5f7fbd4d3e5c8d1" data-id="3b7dfb2b64f881a1b5f7fbd4d3e5c8d1"><span><div id="3b7dfb2b64f881a1b5f7fbd4d3e5c8d1" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881a1b5f7fbd4d3e5c8d1" title="Community &amp; Operations"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Community &amp; Operations</span></span></h4><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8814c9585da9fbfbf8632"><li>Facility Booking: Amenity reservation system (clubhouse, basketball court, pool) with conflict prevention.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881d2a203d76f66507fb7"><li>Service Requests &amp; Ticketing: Residents submit maintenance requests or incident reports directly to HOA officers.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88170bd95d5d37ee1a4e7"><li>Announcement Portal: Broadcast official notices via email (Nodemailer) and dashboard alerts.</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881ae8ac1e59544b3f616"><li>Role-Based Access Control (RBAC): Distinct permissions for Super Admins, HOA Board Members, Staff, and Homeowner Members.</li></ul><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f881b28539fb5997d0ed1f" data-id="3b7dfb2b64f881b28539fb5997d0ed1f"><span><div id="3b7dfb2b64f881b28539fb5997d0ed1f" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881b28539fb5997d0ed1f" title="Technical Architecture &amp; Tech Stack"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Technical Architecture &amp; Tech Stack</span></span></h3><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f88159bd32ef9ba45eb4f6"><li>Framework: Next.js (App Router)</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881b583ebd1af90b1473d"><li>Language: TypeScript</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881e2acacfe2b78adfe6e"><li>Styling &amp; UI: Tailwind CSS, shadcn/ui</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f8818ebfa6c076fd1e6962"><li>Database &amp; ORM: Supabase (Postgres) with Drizzle ORM</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881b389def33b6201a630"><li>Authentication: NextAuth.js / Supabase Auth</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881bca3d6c8f40da04e35"><li>Transactional Email: Nodemailer</li></ul><ul class="notion-list notion-list-disc notion-block-3b7dfb2b64f881919b0ff182fb0490ae"><li>Hosting &amp; Infrastructure: Vercel (Edge Functions &amp; Multi-tenant Routing)</li></ul><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f881cb826ac34e0e253986" data-id="3b7dfb2b64f881cb826ac34e0e253986"><span><div id="3b7dfb2b64f881cb826ac34e0e253986" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881cb826ac34e0e253986" title="Key Technical Challenges &amp; Solutions"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Key Technical Challenges &amp; Solutions</span></span></h3><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f88180a7c9dfe556c4d8b1" data-id="3b7dfb2b64f88180a7c9dfe556c4d8b1"><span><div id="3b7dfb2b64f88180a7c9dfe556c4d8b1" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88180a7c9dfe556c4d8b1" title="1. Multi-Tenant Subdomain Routing"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">1. Multi-Tenant Subdomain Routing</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f88187b40ce3b98302f0ef">Using Next.js Middleware, incoming HTTP requests are inspected for custom hostnames/subdomains. The middleware rewrites the internal URL path to match the target tenant route dynamically while preserving clean URLs for users.</div><h4 class="notion-h notion-h3 notion-h-indent-2 notion-block-3b7dfb2b64f881dfb541d6feaa5bfb82" data-id="3b7dfb2b64f881dfb541d6feaa5bfb82"><span><div id="3b7dfb2b64f881dfb541d6feaa5bfb82" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f881dfb541d6feaa5bfb82" title="2. Tenant Data Isolation with Drizzle ORM &amp; Postgres"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">2. Tenant Data Isolation with Drizzle ORM &amp; Postgres</span></span></h4><div class="notion-text notion-block-3b7dfb2b64f88144a7a8e8d8ae17085d">Every table contains a `tenant_id` foreign key. Database queries are wrapped in tenant-aware helper functions that automatically inject `WHERE tenant_id = current_tenant`, preventing accidental data exposure across organizations.</div><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-3b7dfb2b64f88143a911cfaa709e79a3" data-id="3b7dfb2b64f88143a911cfaa709e79a3"><span><div id="3b7dfb2b64f88143a911cfaa709e79a3" class="notion-header-anchor"></div><a class="notion-hash-link" href="#3b7dfb2b64f88143a911cfaa709e79a3" title="Project Status"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">Project Status</span></span></h3><div class="notion-text notion-block-3b7dfb2b64f881689386f28b1d5952ae">HOACentralPH is in active development and deployment for initial pilot subdivisions across the Philippines.</div></main>]]></content>
    </entry>
</feed>