<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[APEXCrafts by Arfaat]]></title><description><![CDATA[APEXCrafts by Arfaat]]></description><link>https://apexcraftsbyarfaat.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a5086171f31058cc5920051/6c4a7ac3-15c2-48a9-82c9-64f534db79b7.png</url><title>APEXCrafts by Arfaat</title><link>https://apexcraftsbyarfaat.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Thu, 12 Nov 2026 08:55:15 GMT</lastBuildDate><atom:link href="https://apexcraftsbyarfaat.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[One Oracle APEX App, Many Ways to Sign In]]></title><description><![CDATA[Learn how to implement multiple login methods in Oracle APEX using password, Google OAuth, and face recognition with one shared user table.]]></description><link>https://apexcraftsbyarfaat.hashnode.dev/one-oracle-apex-app-many-ways-to-sign-in</link><guid isPermaLink="true">https://apexcraftsbyarfaat.hashnode.dev/one-oracle-apex-app-many-ways-to-sign-in</guid><category><![CDATA[#oracle-apex]]></category><category><![CDATA[Oracle APEX Custom authentication]]></category><category><![CDATA[Multi Factor Authentication]]></category><category><![CDATA[Google OAuth 2.0]]></category><category><![CDATA[Oracle APEX Social Sign-On]]></category><category><![CDATA[Face recognition]]></category><category><![CDATA[PL/SQL]]></category><category><![CDATA[session management]]></category><category><![CDATA[Oracle APEX security features]]></category><category><![CDATA[custom authentication]]></category><dc:creator><![CDATA[Arfaat Shaikh]]></dc:creator><pubDate>Fri, 06 Nov 2026 17:43:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a5086171f31058cc5920051/7425c6c1-905a-4a10-9869-e61bdcf9323a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>An Oracle APEX app can offer several sign-in methods at once: password, Google and even face sign-in, all landing on one user table. The trick is an authentication-scheme setting most of us scroll past: <strong>Switch in Session</strong>.</p>
<p><strong>Try it yourself:</strong></p>
<ul>
<li><a href="https://oracleapex.com/ords/r/apexcrafts/apexcrafts/login">Sign in to APEXCrafts</a>: try password, face or Google sign-in.</li>
<li><a href="https://oracleapex.com/ords/r/apexcrafts/apexcrafts/register">Create a free account</a>: register with email or with your face, then verify with a code.</li>
</ul>
<h2>The myths and the reality</h2>
<p>An APEX app has exactly <strong>one current</strong> authentication scheme, but other schemes in the same app can still be used, one session at a time.</p>
<table>
<thead>
<tr>
<th>What people assume</th>
<th>What actually happens</th>
</tr>
</thead>
<tbody><tr>
<td>One app = one sign-in method</td>
<td>One app = one <strong>default</strong> sign-in method</td>
</tr>
<tr>
<td>Adding Google means replacing your custom login</td>
<td>Google can sit <strong>next to</strong> your custom login</td>
</tr>
<tr>
<td>Each method needs its own app or a complex custom scheme</td>
<td>Each method is its own simple scheme, and you pick one per visit</td>
</tr>
</tbody></table>
<p>The current scheme stays the default for everyone who opens the app normally. Any other scheme with <strong>Switch in Session</strong> enabled can take over for one visitor's session, when a link asks for it.</p>
<h2>How it works</h2>
<p>A link carrying the request <mark>APEX_AUTHENTICATION=</mark><code>&lt;scheme name&gt;</code> tells APEX to use that scheme for this visit, provided the scheme has Switch in Session enabled.</p>
<ol>
<li>The visitor opens your app and lands on the login page of the current scheme (your custom login).</li>
<li>They click Continue with Google. The button opens a protected page with the request APEX_AUTHENTICATION=GOOGLE.</li>
<li>APEX sees the request, switches this session to the scheme named GOOGLE, and sends the visitor to Google.</li>
<li>Google signs them in and returns to APEX, which runs the scheme's post-authentication procedure.</li>
<li>The visitor lands in the app. Everyone else keeps using the default login.</li>
</ol>
<p>The link itself is plain, so a button or a menu entry can trigger it:</p>
<pre><code class="language-javascript">// "Continue with Google" button on the login page
apex.navigation.redirect(
  '/ords/f?p=' + apex.item('pFlowId').getValue() + ':1:' +
  apex.item('pInstance').getValue() + ':APEX_AUTHENTICATION=GOOGLE');
</code></pre>
<p>The scheme is found by its <strong>name</strong>, so give it a short, space-free name like <code>GOOGLE</code>.</p>
<h2>Real example: APEXCrafts</h2>
<p>APEXCrafts offers three sign-in methods, and every one of them ends on the same row in one table, <code>APEXCRAFT_USERS</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a5086171f31058cc5920051/02455f39-1540-4345-b716-d93e25b96e8d.png" alt="" style="display:block;margin:0 auto" />

<p>Password and Google each use their own authentication scheme. Face sign-in reuses the custom scheme's session through <code>post_login</code>, and all three fill the same session items, so the rest of the app never cares how someone signed in.</p>
<h2>Step-by-step setup</h2>
<p>You need two schemes, both with Switch in Session enabled, and one post-authentication procedure that maps every sign-in to the same user row.</p>
<h3>1. Keep your custom scheme as the current one</h3>
<p>In <strong>Shared Components → Authentication Schemes</strong>, the custom scheme stays <strong>Current</strong>. Mine calls a PL/SQL function that checks the email and a salted, hashed password.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a5086171f31058cc5920051/c68e7a3d-90f3-4349-8426-d1209a077535.png" alt="" style="display:block;margin:0 auto" />

<h3>2. Store the Google keys in a Web Credential</h3>
<p>Create an OAuth client in Google Cloud (type: Web application). Its redirect URI is the APEX callback, not one of your pages:</p>
<p><a href="https:///ords/apex_authentication.callback"><mark>https:///ords/apex_authentication.callback</mark></a></p>
<p>Then create a Web Credential in APEX with <strong>Authentication Type: OAuth2 Client Credentials</strong>, holding the Client ID and Client Secret.</p>
<h3>3. Add the Google scheme next to it</h3>
<table>
<thead>
<tr>
<th>Setting</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td>Name</td>
<td><code>GOOGLE</code></td>
</tr>
<tr>
<td>Scheme Type</td>
<td>Social Sign-In</td>
</tr>
<tr>
<td>Authentication Provider</td>
<td>Google</td>
</tr>
<tr>
<td>Credential Store</td>
<td>your Web Credential</td>
</tr>
<tr>
<td>Scope</td>
<td><code>profile,email</code></td>
</tr>
<tr>
<td>Username</td>
<td><code>email</code></td>
</tr>
<tr>
<td>Additional User Attributes</td>
<td><code>sub,name,picture,email_verified</code></td>
</tr>
<tr>
<td>Post-Authentication Procedure Name</td>
<td><code>apexcraft_google_pkg.post_auth</code></td>
</tr>
<tr>
<td>Switch in Session</td>
<td><strong>Enabled</strong></td>
</tr>
</tbody></table>
<p>Do <strong>not</strong> click <em>Make Current Scheme</em> on this one. If you do, your custom login stops being the default.</p>
<h3>4. Map Google users to your own table</h3>
<p>The post-authentication procedure reads what Google returned through <code>APEX_JSON</code> and decides who the person is. Three cases cover everyone:</p>
<pre><code class="language-sql">l_sub   := apex_json.get_varchar2('sub');            -- Google's permanent user ID
l_email := lower(apex_json.get_varchar2('email'));

-- 1) seen this Google account before    -&gt; sign in that member
-- 2) same mailbox as an existing member -&gt; link Google to that member (no duplicate account)
-- 3) brand-new person                    -&gt; create a MEMBER row, email already verified
</code></pre>
<p>Case 2 matters most. Without it, a member who first registered with a password would get a <strong>second</strong> account the first time they use Google.</p>
<h3>5. Remember who is signed in, the same way for every method</h3>
<p>Every scheme ends by calling one shared procedure that fills a few <strong>restricted</strong> application items (<code>AI_USER_ID</code>, <code>AI_USERNAME</code>, <code>AI_USER_ROLE</code>). Authorization schemes and pages then use those items, and never care which method was used to sign in.</p>
<h2>Bonus: a third way in, without a third scheme</h2>
<p>Face sign-in in APEXCrafts needs no scheme of its own: the server checks the face, then finishes the login with <code>apex_authentication.post_login</code>.</p>
<p>The browser turns the face into a 128-number signature (with the open-source face-api library), and the database compares it with the stored one using <code>VECTOR_DISTANCE</code>. A login can't be completed inside an AJAX call, so it takes two steps:</p>
<ol>
<li>The check (AJAX). On a match, the server writes the member's email and a timestamp into two application items set to Restricted - May not be set from browser.</li>
<li>The sign-in (page submit). A process reads those items, accepts them only if they're under 60 seconds old, clears them, and signs the member in.</li>
</ol>
<pre><code class="language-sql">-- process "Face sign-in", runs only for request FACE_SIGNIN
l_email := :AI_FACE_EMAIL;
apex_util.set_session_state('AI_FACE_EMAIL', null);   -- one-time use
apex_util.set_session_state('AI_FACE_TIME',  null);
-- ... reject if missing or older than 60 seconds ...
apex_authentication.post_login(p_username =&gt; l_email);
</code></pre>
<p>Because only the server can write those items, a browser can't fake a face match. The same pattern works for any check you run yourself, such as a magic link or a one-time code.</p>
<h2>Gotchas I hit, and the fixes</h2>
<table>
<thead>
<tr>
<th>Symptom</th>
<th>Cause</th>
<th>Fix</th>
</tr>
</thead>
<tbody><tr>
<td>Google sign-in works, but no user row is created or linked</td>
<td>The post-authentication procedure name wasn't saved on the <code>GOOGLE</code> scheme</td>
<td>Re-open the scheme, check <strong>Login Processing → Post-Authentication Procedure Name</strong>, and apply changes</td>
</tr>
<tr>
<td>Email and password stop working after adding Google</td>
<td><em>Make Current Scheme</em> was clicked on the Google scheme</td>
<td>Make the custom scheme current again</td>
</tr>
<tr>
<td>Google shows <code>redirect_uri_mismatch</code></td>
<td>The redirect URI points at an app page</td>
<td>Use <code>https://&lt;host&gt;/ords/apex_authentication.callback</code>, exactly</td>
</tr>
<tr>
<td>Google says "access blocked" for most accounts</td>
<td>The consent screen is still in <strong>Testing</strong> mode</td>
<td>Add test users, or publish the app (basic scopes need no review)</td>
</tr>
<tr>
<td>Login and sign-up links break after turning on Friendly URLs</td>
<td>Relative links like <code>f?p=…</code> resolve under <code>/r/&lt;workspace&gt;/&lt;app&gt;/</code></td>
<td>Start links with <code>/ords/f?p=…</code></td>
</tr>
<tr>
<td>"No checksum was provided" when a link opens a page</td>
<td>Page Access Protection requires a checksum for any URL argument</td>
<td>Set public pages to <em>Unrestricted</em>, and protect their items individually</td>
</tr>
<tr>
<td><code>PLS-00231: function may not be used in SQL</code></td>
<td>A private package function is called inside an <code>INSERT</code> or <code>SELECT</code></td>
<td>Assign the result to a variable first, then use the variable</td>
</tr>
<tr>
<td>Normal login shows "Your face sign-in expired"</td>
<td>The face process runs on every submit</td>
<td>Give it a condition: <strong>Request = FACE_SIGNIN</strong></td>
</tr>
</tbody></table>
<h2>Security checklist</h2>
<p>More ways in means more doors to lock, so check each of these before going live.</p>
<ul>
<li><strong>One identity per person.</strong> Link methods by Google's <code>sub</code> first, then by email. Compare emails in a canonical form (lowercase, Gmail dots and <code>+tags</code> removed) so aliases can't create duplicate accounts.</li>
<li><strong>Never trust the browser.</strong> Identity proofs live in application items with <em>Restricted - May not be set from browser</em>, and expire quickly.</li>
<li><strong>Same rules for every method.</strong> Locked and inactive accounts are refused in the custom function and in every post-authentication procedure.</li>
<li><strong>Secrets stay in Web Credentials.</strong> API keys and client secrets never appear in code, and exports never include them.</li>
<li><strong>Roles come from your table.</strong> Authorization schemes read the role from the database on each page view, not from how the user signed in.</li>
<li>Recovery leaks nothing it shouldn't**.** Recovery emails go only to real accounts, and codes are rate-limited per mailbox. If you show live "account found" checks, apply the same rule to sign-up and recovery, and decide that trade-off on purpose.</li>
</ul>
<h2>Wrap-up</h2>
<p>One current scheme is APEX's default, not its limit: with <strong>Switch in Session</strong>, one app can offer password, Google and custom methods side by side.</p>
<p>The pattern is short to remember:</p>
<ol>
<li>Keep your main login as the current scheme.</li>
<li>Add each extra method as its own scheme, with Switch in Session enabled.</li>
<li>Point a button at APEX_AUTHENTICATION=.</li>
<li>Send every scheme through post-authentication logic that maps people to one user table.</li>
</ol>
<p>You can see the result live in <strong>APEXCrafts</strong>, the community app I'm building for Oracle APEX developers. If you try this pattern, or you've solved multi-method sign-in differently, tell me in the comments. I'd love to compare notes.</p>
<p><strong>Try it yourself:</strong></p>
<ul>
<li><a href="https://oracleapex.com/ords/r/apexcrafts/apexcrafts/login">Sign in to APEXCrafts</a>: try password, face or Google sign-in.</li>
<li><a href="https://oracleapex.com/ords/r/apexcrafts/apexcrafts/register">Create a free account</a>: register with email or with your face, then verify with a code.</li>
</ul>
<p><strong>Thank You for your time.</strong></p>
<p>If you need any help or have any questions, feel free to leave a comment or contact me via email.</p>
<p><a href="mailto:arfaats22@gmail.com">arfaats22@gmail.com</a><br /><a href="mailto:support.apexcrafts@gmail.com">support.apexcrafts@gmail.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Build a Free AI Assistant Agent in Oracle APEX]]></title><description><![CDATA[Learn how to build an AI project assistant in Oracle APEX using RAG, AI Agents and Cohere.]]></description><link>https://apexcraftsbyarfaat.hashnode.dev/build-a-free-ai-assistant-agent-in-oracle-apex</link><guid isPermaLink="true">https://apexcraftsbyarfaat.hashnode.dev/build-a-free-ai-assistant-agent-in-oracle-apex</guid><category><![CDATA[#oracle-apex]]></category><category><![CDATA[Low Code]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[rag chatbot]]></category><category><![CDATA[ai-agent]]></category><category><![CDATA[Pipeline]]></category><category><![CDATA[#ALXCommunity]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Arfaat Shaikh]]></dc:creator><pubDate>Tue, 27 Oct 2026 16:25:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a5086171f31058cc5920051/3dc7c52b-8882-4da4-8d35-e76aa58d5b8d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>1. What we are building</h2>
<p>A chat assistant inside an Oracle APEX app. You ask questions like <em>"Which projects are over budget?"</em> and it answers <strong>only from your own project data</strong>.</p>
<p><strong>How it works (simple version):</strong></p>
<pre><code class="language-plaintext">You ask a question
      ↓
APEX AI Agent picks the right Tool (a SQL query)
      ↓
Tool returns only the allowed data
      ↓
Cohere writes a short answer from that data
</code></pre>
<p>This is <strong>RAG</strong> (Retrieval-Augmented Generation): the AI does not guess from the internet. It reads the data your tools give it, and nothing else. That is how we <strong>limit the AI to our data</strong>.</p>
<p><strong>Why this design is easy to maintain:</strong> there is no custom code to debug. Everything is APEX settings plus 5 small SQL queries.</p>
<table>
<thead>
<tr>
<th>Sample APP</th>
<th>Url</th>
</tr>
</thead>
<tbody><tr>
<td>Oracle APEX</td>
<td><a href="https://oracleapex.com/ords/f?p=218621:2">https://oracleapex.com/ords/f?p=218621:2</a></td>
</tr>
</tbody></table>
<h2>2. What is free, and what are the limits</h2>
<table>
<thead>
<tr>
<th>Part</th>
<th>Cost</th>
<th>Limit to know</th>
</tr>
</thead>
<tbody><tr>
<td>Oracle APEX (Always Free)</td>
<td>Free</td>
<td>Small shared database, fine for learning</td>
</tr>
<tr>
<td>Sample data</td>
<td>Free</td>
<td>Tiny, uses almost no space</td>
</tr>
<tr>
<td>Cohere trial key</td>
<td>Free</td>
<td><strong>1,000 API calls per month</strong>, about 20 requests per minute, <strong>not for commercial use</strong></td>
</tr>
</tbody></table>
<p><strong><mark>Be honest with yourself:</mark></strong> <mark>a trial key is perfect for learning, demos, and a portfolio. For a real business app, you will need a paid Cohere key. The good news: you only change the key in one place (Step 3).</mark></p>
<p><strong><mark>Tip:</mark></strong> <mark>one question can use 2 or more calls (the agent picks a tool, then answers). Plan for roughly 300 to 400 questions a month.</mark></p>
<h2>3. Step 1: Create your free Oracle APEX workspace</h2>
<ol>
<li>Sign up at <a href="http://cloud.oracle.com">cloud.oracle.com</a> (choose the Always Free option).</li>
<li>Create an APEX Service and select Always Free.</li>
<li>Save your ADMIN password safely.</li>
<li>Open APEX and create a workspace.</li>
</ol>
<h2>4. Step 2: Get your free Cohere API key</h2>
<ol>
<li>Sign up at <a href="http://dashboard.cohere.com">dashboard.cohere.com</a>.</li>
<li>Open API Keys and copy your Trial key.<br /><mark>Security rule: never paste your key in a blog, screenshot, or GitHub. If it leaks, delete it in the Cohere dashboard and create a new one.</mark></li>
</ol>
<h2>5. Step 3: Store the key safely (Web Credential)</h2>
<p>In APEX go to <strong>Workspace Utilities → Web Credentials → Create</strong> and fill in:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Enter</th>
</tr>
</thead>
<tbody><tr>
<td>Name</td>
<td><code>COHERE API</code></td>
</tr>
<tr>
<td>Static ID</td>
<td><code>cohere_api</code></td>
</tr>
<tr>
<td>Authentication Type</td>
<td><code>HTTP Header</code></td>
</tr>
<tr>
<td>Credential Name</td>
<td><code>Authorization</code></td>
</tr>
<tr>
<td>Credential Secret</td>
<td><code>Bearer YOUR_COHERE_KEY</code></td>
</tr>
<tr>
<td>Valid for URLs</td>
<td><code>https://api.cohere.com/</code></td>
</tr>
<tr>
<td>Prompt On Install</td>
<td><code>On</code></td>
</tr>
</tbody></table>
<p><strong>Notes:</strong></p>
<ul>
<li><mark>Type the word</mark> <code>Bearer</code><mark>, then</mark> <strong><mark>one space</mark></strong><mark>, then your key.</mark></li>
<li><mark>"Valid for URLs" makes sure the key is only ever sent to Cohere.</mark></li>
<li><mark>Later, to move to a paid key, edit</mark> <strong><mark>only this secret</mark></strong><mark>. Nothing else changes.</mark></li>
</ul>
<p>Click <strong>Create</strong>.</p>
<h2>6. Step 4: Install the sample data</h2>
<p>Go to <strong>App Builder → Gallery (or Sample Datasets)</strong> and install <strong>Project Data</strong>.</p>
<p><mark>Why this dataset? It has real text and business meaning (projects, milestones, tasks, comments), which suits an AI assistant. It is small, so it will not use your storage.</mark></p>
<p>It creates these tables and views:<br />EBA_PROJECTS, EBA_PROJECT_STATUS,<br />EBA_PROJECT_MILESTONES<br />EBA_PROJECT_TASKS<br />EBA_PROJECT_TASK_TODOS<br />EBA_PROJECT_TASK_LINKS<br />EBA_PROJECT_COMMENTS<br />Views: EBA_PROJECTS_V<br />EBA_PROJECTS_COMPLETED_V</p>
<h2>7. Step 5: Connect APEX to Cohere (AI Service)</h2>
<p>Go to <strong>Workspace Utilities → Generative AI → AI Services → Create</strong>.</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Enter</th>
</tr>
</thead>
<tbody><tr>
<td>AI Provider</td>
<td><code>Cohere</code></td>
</tr>
<tr>
<td>Name</td>
<td><code>Cohere AI</code></td>
</tr>
<tr>
<td>Base URL</td>
<td><code>https://api.cohere.com/v2</code></td>
</tr>
<tr>
<td>AI Model</td>
<td>A current Cohere <strong>Command</strong> model (check Cohere's model list for the latest name)</td>
</tr>
<tr>
<td>Credential</td>
<td><code>cohere_api</code></td>
</tr>
<tr>
<td>Static ID</td>
<td><code>cohere-ai</code></td>
</tr>
</tbody></table>
<p><strong>Important:</strong> <mark>the Base URL domain (</mark><code>api.cohere.com</code><mark>) must match the "Valid for URLs" value from Step 3, or the call can be blocked.</mark></p>
<p>Click <strong>Test Connection</strong>. When it succeeds, click <strong>Create</strong>.</p>
<h2>8. Step 6: Create the 5 Tools (this is the data limit)</h2>
<p>A <strong>Tool</strong> is a SQL query the AI is allowed to use. If data is not in a tool, the AI cannot see it.</p>
<p>For each tool, set <strong>Type = Retrieve Data</strong>.</p>
<p><strong>Before you add each one, run its SQL in SQL Workshop.</strong> If it returns rows, it will work in the tool.</p>
<h3>Tool 1: Projects</h3>
<ul>
<li><strong>Name:</strong> <code>Projects</code></li>
<li><strong>Execution Point:</strong> <code>Augment System Prompt</code> (always loaded, because it is small and almost every question needs it)</li>
<li><strong>Description:</strong> List of all projects with status, lead, budget, cost, remaining budget, over-budget flag, task progress, milestone count, comment count and completion date. Use for any question about projects, budgets or costs.</li>
</ul>
<pre><code class="language-sql">select 'PROJECT: ' || p.name
    || ' | Status: '      || nvl(s.description, 'Unknown')
    || ' | Lead: '        || nvl(p.project_lead, 'Not set')
    || ' | Budget: '      || nvl(p.budget, 0)
    || ' | Cost: '        || nvl(t.cost, 0)
    || ' | Remaining: '   || (nvl(p.budget, 0) - nvl(t.cost, 0))
    || case when nvl(p.budget, 0) - nvl(t.cost, 0) &lt; 0 then ' (OVER BUDGET)' end
    || ' | Tasks done: '  || nvl(t.done, 0) || ' of ' || nvl(t.total, 0)
    || ' | Milestones: '  || (select count(*) from eba_project_milestones m where m.project_id = p.id)
    || ' | Comments: '    || (select count(*) from eba_project_comments c where c.project_id = p.id)
    || ' | Completed on: '|| nvl(to_char(p.completed_date, 'DD-MON-YYYY'), '-')
    || ' | Today: '       || to_char(sysdate, 'DD-MON-YYYY') as info
  from eba_projects p
  left join eba_project_status s on s.id = p.status_id
  left join (select project_id,
                    sum(cost) cost,
                    count(*)  total,
                    count(case when nvl(is_complete_yn,'N') = 'Y' then 1 end) done
               from eba_project_tasks
              group by project_id) t
         on t.project_id = p.id
 order by p.name
</code></pre>
<h3>Tool 2: Milestones</h3>
<ul>
<li><strong>Name:</strong> <code>Milestones</code></li>
<li><strong>Execution Point:</strong> <code>On Demand</code></li>
<li><strong>Description:</strong> All milestones with their project, due date, task progress and status (ACHIEVED, OVERDUE, IN PROGRESS, NO TASKS). Use for questions about milestones, deadlines, due dates or what has been achieved.</li>
</ul>
<pre><code class="language-sql">select 'MILESTONE: ' || m.name
    || ' | Project: '    || p.name
    || ' | Due: '        || to_char(m.due_date, 'DD-MON-YYYY')
    || ' | Tasks done: ' || nvl(x.done, 0) || ' of ' || nvl(x.total, 0)
    || ' | Status: ' ||
       case when nvl(x.total, 0) = 0             then 'NO TASKS'
            when x.done = x.total                then 'ACHIEVED'
            when m.due_date &lt; trunc(sysdate)     then 'OVERDUE'
            else 'IN PROGRESS' end as info
  from eba_project_milestones m
  join eba_projects p on p.id = m.project_id
  left join (select milestone_id,
                    count(*) total,
                    count(case when nvl(is_complete_yn,'N') = 'Y' then 1 end) done
               from eba_project_tasks
              where milestone_id is not null
              group by milestone_id) x
         on x.milestone_id = m.id
 order by m.due_date
</code></pre>
<h3>Tool 3: Tasks</h3>
<ul>
<li><strong>Name:</strong> <code>Tasks</code></li>
<li><strong>Execution Point:</strong> <code>On Demand</code></li>
<li><strong>Description:</strong> All tasks with project, milestone, assignee, start and end dates, cost and status (DONE, OVERDUE, OPEN). Use for questions about tasks, assignees, workload, overdue or late work.</li>
</ul>
<pre><code class="language-sql">select 'TASK: ' || t.name
    || ' | Project: '   || p.name
    || ' | Milestone: ' || nvl(m.name, '-')
    || ' | Assignee: '  || nvl(t.assignee, 'Unassigned')
    || ' | Start: '     || to_char(t.start_date, 'DD-MON-YYYY')
    || ' | End: '       || to_char(t.end_date, 'DD-MON-YYYY')
    || ' | Cost: '      || nvl(t.cost, 0)
    || ' | Status: ' ||
       case when nvl(t.is_complete_yn,'N') = 'Y' then 'DONE'
            when t.end_date &lt; trunc(sysdate)     then 'OVERDUE'
            else 'OPEN' end as info
  from eba_project_tasks t
  join eba_projects p on p.id = t.project_id
  left join eba_project_milestones m on m.id = t.milestone_id
 order by p.name, t.end_date
</code></pre>
<h3>Tool 4: To-Dos</h3>
<ul>
<li><strong>Name:</strong> <code>Task_ToDos</code></li>
<li><strong>Execution Point:</strong> <code>On Demand</code></li>
</ul>
<p><strong>Description:</strong> Checklist to-do items inside tasks, with their task, project, assignee and status (DONE or OPEN). Use for questions about to-dos, checklist items or small pending actions.</p>
<pre><code class="language-sql">select 'TO-DO: ' || d.name
    || ' | Task: '     || t.name
    || ' | Project: '  || p.name
    || ' | Assignee: ' || nvl(d.assignee, 'Unassigned')
    || ' | Status: '   || case when nvl(d.is_complete_yn,'N') = 'Y'
                               then 'DONE' else 'OPEN' end as info
  from eba_project_task_todos d
  join eba_project_tasks t on t.id = d.task_id
  join eba_projects p      on p.id = d.project_id
 order by p.name, t.name
</code></pre>
<h3>Tool 5: Comments</h3>
<ul>
<li><strong>Name:</strong> <code>Comments</code></li>
<li><strong>Execution Point:</strong> <code>On Demand</code></li>
<li><strong>Description:</strong> Comments written on projects, with project name, author and date (latest 10 per project). Comments belong to projects, not milestones or tasks. Use for questions about comments, updates, notes, issues or what people said.</li>
</ul>
<pre><code class="language-sql">select 'COMMENT on project ' || project_name
    || ' | By: '   || created_by
    || ' | Date: ' || to_char(created, 'DD-MON-YYYY')
    || ' | Text: ' || substr(comment_text, 1, 500) as info
  from (select p.name as project_name, c.created, c.created_by, c.comment_text,
               row_number() over (partition by c.project_id
                                  order by c.created desc) rn
          from eba_project_comments c
          join eba_projects p on p.id = c.project_id)
 where rn &lt;= 10
 order by project_name, created desc
</code></pre>
<p><strong>Why "Execution Point" matters:</strong></p>
<ul>
<li><code>Augment System Prompt</code> = sent with every question (use only for small, always-needed data).</li>
<li><code>On Demand</code> = the AI calls the tool only when the question needs it. This saves tokens and keeps answers focused.</li>
</ul>
<p><strong>Why the Description matters:</strong> the AI reads the description to decide <em>which</em> tool to call. Write it clearly, and say what the tool is for.</p>
<h2>9. Step 7: Create the AI Agent</h2>
<p>Go to Generative AI → AI Agents → Create.</p>
<ol>
<li>Select the AI Service Cohere AI.</li>
<li>Attach all 5 tools.</li>
<li>Paste this System Prompt:</li>
</ol>
<pre><code class="language-plaintext">You are a project assistant. Answer ONLY from the data returned by your tools.
Every record includes "Project: &lt;name&gt;". Use the project name to connect data
from different tools (for example, milestones and comments of the same project).
Comments belong to projects, not to milestones or tasks.
If the answer is not in the data, say "I couldn't find that in the project data."
Keep answers short. Use a small table when listing more than 3 items.
</code></pre>
<p>Paste this <strong>Welcome Message</strong>:</p>
<pre><code class="language-plaintext">Hi! 👋 I'm your Project Assistant. Ask me about projects, tasks, milestones, or comments, and I'll answer from the project data.
</code></pre>
<p><strong>Why this prompt works:</strong></p>
<ul>
<li>"Answer ONLY from the data" is the data limit.</li>
<li>The fixed fallback sentence makes refusals predictable.</li>
<li>"Keep answers short" saves tokens and reads better.</li>
</ul>
<p>(Menu names can differ slightly between APEX versions. Follow the closest option.)</p>
<h2>10. Step 8: Add the Ask AI button to a page</h2>
<ol>
<li>Create Page → Blank Page, name it AI Assistant.</li>
<li>In Page Designer, right-click a region → Create Button:</li>
</ol>
<table>
<thead>
<tr>
<th>Field</th>
<th>Enter</th>
</tr>
</thead>
<tbody><tr>
<td>Button Name</td>
<td><code>ASK_AI</code></td>
</tr>
<tr>
<td>Label</td>
<td><code>Ask AI</code></td>
</tr>
<tr>
<td>Action</td>
<td><code>Defined by Dynamic Action</code></td>
</tr>
</tbody></table>
<ol start="3">
<li>Right-click the button → <strong>Create Dynamic Action</strong>:</li>
</ol>
<table>
<thead>
<tr>
<th>Field</th>
<th>Enter</th>
</tr>
</thead>
<tbody><tr>
<td>Event</td>
<td><code>Click</code></td>
</tr>
<tr>
<td>True Action</td>
<td><code>Show AI Assistant</code></td>
</tr>
<tr>
<td>AI Configuration / Agent</td>
<td>your agent</td>
</tr>
<tr>
<td>Display As</td>
<td><code>Dialog</code></td>
</tr>
</tbody></table>
<ol start="4">
<li>Click <strong>Save and Run</strong>, then press <strong>Ask AI</strong>.</li>
</ol>
<h2>11. Step 9: Test it</h2>
<p>Use these questions. Do not run them all every day, because each one uses your free Cohere calls.</p>
<p><strong>Projects</strong></p>
<ul>
<li>Which projects are over budget?</li>
<li>Show me all in-progress projects</li>
<li>What is the total budget and total cost?</li>
<li>Which project has the most budget remaining?</li>
</ul>
<p><strong>Milestones</strong></p>
<ul>
<li>Which milestones are achieved?</li>
<li>Which milestones are overdue?</li>
<li>What milestones are due this month?</li>
</ul>
<p><strong>Tasks</strong></p>
<ul>
<li>Show me all overdue tasks</li>
<li>Who has the most open tasks?</li>
<li>Which tasks are unassigned?</li>
</ul>
<p><strong>To-Dos</strong></p>
<ul>
<li>What to-dos are still open?</li>
</ul>
<p><strong>Comments</strong></p>
<ul>
<li>Summarize the latest comments</li>
<li>Which projects have issues mentioned in comments?</li>
</ul>
<p><strong>Mixed (several tools at once)</strong></p>
<ul>
<li>Which achieved milestones have comments on their project?</li>
<li>Give me a quick health summary of all projects</li>
<li>Which over-budget projects also have overdue tasks?</li>
</ul>
<p><strong>Scope test (must refuse):</strong></p>
<ul>
<li>Who is the Prime Minister of India?</li>
</ul>
<p>Expected reply: <em>"I couldn't find that in the project data."</em></p>
<p><strong><mark>Quality tip:</mark></strong> <mark>keep a simple table of question, expected answer, and pass or fail. Re-run a few of them after every change.</mark></p>
<h2>12. Troubleshooting</h2>
<table>
<thead>
<tr>
<th>Problem</th>
<th>Likely cause</th>
<th>Fix</th>
</tr>
</thead>
<tbody><tr>
<td>Test Connection fails with 401</td>
<td>Wrong key, or <code>Bearer</code> missing</td>
<td>Re-enter the credential secret</td>
</tr>
<tr>
<td>Test Connection is blocked</td>
<td>Base URL does not match "Valid for URLs"</td>
<td>Use <code>https://api.cohere.com/v2</code> and <code>https://api.cohere.com/</code></td>
</tr>
<tr>
<td>Tool returns an error</td>
<td>A column name in the SQL is wrong</td>
<td>Run the SQL in SQL Workshop and fix it</td>
</tr>
<tr>
<td>Answer says "not found" for real data</td>
<td>Tool returns 0 rows, or the description is unclear</td>
<td>Check the SQL, and improve the tool description</td>
</tr>
<tr>
<td>"Trial keys are throttled" error</td>
<td>Too many requests per minute</td>
<td>Wait one minute and try again</td>
</tr>
<tr>
<td>Fails for the rest of the month</td>
<td>Monthly 1,000 calls used up</td>
<td>Wait for the next month, or use a paid key</td>
</tr>
</tbody></table>
]]></content:encoded></item><item><title><![CDATA[Integrating Razorpay with Oracle APEX Using a Free Node.js Backend on Render]]></title><description><![CDATA[Integrate Razorpay with Oracle APEX using a secure Node.js backend deployed free on Render with GitHub integration.]]></description><link>https://apexcraftsbyarfaat.hashnode.dev/integrating-razorpay-with-oracle-apex-using-a-free-node-js-backend-on-render</link><guid isPermaLink="true">https://apexcraftsbyarfaat.hashnode.dev/integrating-razorpay-with-oracle-apex-using-a-free-node-js-backend-on-render</guid><category><![CDATA[#oracle-apex]]></category><category><![CDATA[razorpay]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Express.js]]></category><category><![CDATA[render]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[payment gateway]]></category><category><![CDATA[REST API]]></category><category><![CDATA[#lowcodedevelopment]]></category><category><![CDATA[webdevelopment]]></category><category><![CDATA[Secure Payments]]></category><category><![CDATA[cloud deployment]]></category><category><![CDATA[production-ready]]></category><category><![CDATA[100PercentFree]]></category><dc:creator><![CDATA[Arfaat Shaikh]]></dc:creator><pubDate>Fri, 17 Jul 2026 11:27:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a5086171f31058cc5920051/3cb44e85-85e4-4b17-897a-002b6512e23b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><mark class="bg-yellow-200 dark:bg-yellow-500/30">Demo App - </mark> <mark class="bg-yellow-200 dark:bg-yellow-500/30"><a href="https://oracleapex.com/ords/r/tecverse/eventrix280903/login">https://oracleapex.com/ords/r/tecverse/eventrix280903/login</a></mark><br /><mark class="bg-yellow-200 dark:bg-yellow-500/30">Create a new User and try.</mark></p>
<p><strong>Introduction</strong></p>
<p>Oracle APEX applications often require payment functionality for bookings, registrations, subscriptions, and event management systems. While Razorpay provides an excellent checkout experience, one challenge quickly appears:</p>
<p>Razorpay Order APIs require the Secret Key, and exposing secret keys directly in Oracle APEX JavaScript is insecure.</p>
<h3><strong>The solution is simple:</strong></h3>
<ul>
<li><p>Oracle APEX handles the frontend.</p>
</li>
<li><p>A small Node.js backend securely creates Razorpay orders.</p>
</li>
<li><p>Render hosts the backend for free.</p>
</li>
<li><p>GitHub stores and manages the backend code.</p>
</li>
</ul>
<p>This guide walks through the complete process from local development to deployment and integration with Oracle APEX.</p>
<h2>Architecture Overview</h2>
<p>The payment flow looks like this:</p>
<pre><code class="language-plaintext">Oracle APEX
     |
     | Request order creation
     ↓
Node.js Backend (Render)
     |
     | Uses Razorpay Secret Key
     ↓
Razorpay Orders API
     |
     | Returns order_id
     ↓
Oracle APEX
     |
     | Opens Razorpay Checkout
     ↓
Customer Payment
</code></pre>
<p>The important principle is:</p>
<blockquote>
<p>Secret keys never leave the backend.</p>
</blockquote>
<h3><strong>Step 1: Install Node.js</strong></h3>
<p>Download and install the LTS version of Node.js:</p>
<p><a href="https://nodejs.org">https://nodejs.org</a></p>
<p>Verify installation:</p>
<pre><code class="language-plaintext">node -v
npm -v
</code></pre>
<p>Example output:</p>
<pre><code class="language-plaintext">v24.11.0
11.6.2
</code></pre>
<h3></h3>
<p><strong>Step 2: Create Project Folder</strong></p>
<p>Create a folder for the backend.</p>
<pre><code class="language-plaintext">mkdir razorpay-backend
cd razorpay-backend
</code></pre>
<h3>Step 3: Initialize Node Project</h3>
<p>Create a package.json file:</p>
<pre><code class="language-plaintext">npm init -y
</code></pre>
<h3></h3>
<p><strong>Step 4: Install Required Packages</strong></p>
<p>Install the dependencies:</p>
<pre><code class="language-plaintext">npm install express cors axios
</code></pre>
<p>Installed packages:</p>
<ul>
<li><p>Express → Web server</p>
</li>
<li><p>CORS → Allows Oracle APEX to call the backend</p>
</li>
<li><p>Axios → Calls Razorpay APIs</p>
</li>
</ul>
<h3><strong>Step 5: Create index.js</strong></h3>
<p>Create a file named: <strong>index.js</strong></p>
<p>Paste the following code:</p>
<pre><code class="language-plaintext">const express = require('express'); const cors = require('cors'); const axios = require('axios');

const app = express();

app.use(cors()); app.use(express.json());

const RZP_KEY_ID = 'YOUR_TEST_KEY_ID'; const RZP_KEY_SECRET = 'YOUR_TEST_SECRET';

app.get('/', (req, res) =&gt; { res.send('Razorpay backend up'); });

app.post('/create-order', async (req, res) =&gt; { try {
const amountRupees = Number(req.body.amount) || 0;
const bookingRef = req.body.booking_ref || 'NO_REF';

const amountPaise = Math.round(amountRupees * 100);

const auth = Buffer.from(
  RZP_KEY_ID + ':' + RZP_KEY_SECRET
).toString('base64');

const response = await axios.post(
  'https://api.razorpay.com/v1/orders',
  {
    amount: amountPaise,
    currency: 'INR',
    receipt: bookingRef
  },
  {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Basic ' + auth
    }
  }
);

res.json({
  success: true,
  order_id: response.data.id,
  amount: response.data.amount,
  currency: response.data.currency
});
} catch(err) {
res.status(500).json({
  success: false,
  error: err.message
});
}});
const PORT = process.env.PORT || 3000;

app.listen(PORT, () =&gt; { console.log('Server listening on port', PORT); });
</code></pre>
<h3>Step 6: Run the Backend Locally</h3>
<p>Start the server:</p>
<pre><code class="language-plaintext">node index.js
</code></pre>
<p>Expected output:</p>
<pre><code class="language-plaintext">Server listening on port 3000
</code></pre>
<p>Open:</p>
<pre><code class="language-plaintext">http://localhost:3000
</code></pre>
<p>Expected result:</p>
<pre><code class="language-plaintext">Razorpay backend up
</code></pre>
<h3><strong>Step 7: Create GitHub Repository</strong></h3>
<p>Create a repository:</p>
<pre><code class="language-plaintext">razorpay-backend
</code></pre>
<p>Initialize Git:</p>
<pre><code class="language-plaintext">git init
git add .
git commit -m "Initial Razorpay backend"
</code></pre>
<p>Connect GitHub:</p>
<pre><code class="language-plaintext">git branch -M main
git remote add origin https://github.com/your-username/razorpay-backend.git
git push -u origin main
</code></pre>
<h3><strong>Step 8: Deploy on Render</strong></h3>
<p>Visit:</p>
<pre><code class="language-plaintext">https://render.com
</code></pre>
<p>Login using GitHub.</p>
<p>Create:</p>
<pre><code class="language-plaintext">New → Web Service
</code></pre>
<p>Select:</p>
<pre><code class="language-plaintext">razorpay-backend
</code></pre>
<p>Configure:</p>
<table>
<thead>
<tr>
<th>Setting</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td>Environment</td>
<td>Node</td>
</tr>
<tr>
<td>Build Command</td>
<td>npm install</td>
</tr>
<tr>
<td>Start Command</td>
<td>node index.js</td>
</tr>
<tr>
<td>Plan</td>
<td>Free</td>
</tr>
</tbody></table>
<h3><strong>Deploy the service.</strong></h3>
<p>Render provides a URL similar to:</p>
<pre><code class="language-plaintext">https://razorpay-backend-xxxx.onrender.com
</code></pre>
<h3><strong>Step 9: Test Deployment</strong></h3>
<p>Open:</p>
<pre><code class="language-plaintext">https://razorpay-backend-xxxx.onrender.com
</code></pre>
<p>Expected output:</p>
<pre><code class="language-plaintext">Razorpay backend up
</code></pre>
<h3>Step 10: Test Order Creation API</h3>
<p>Example request:</p>
<blockquote>
<p><code>curl -X POST https://your-backend.onrender.com/create-order   -H "Content-Type: application/json"   -d "{"amount":1000,"booking_ref":"BOOK_001"}"</code></p>
</blockquote>
<p>Expected response:</p>
<p>{ "success": true, "order_id": "order_Q123456789", "amount": 100000, "currency": "INR" }  </p>
<p><strong>Step 11: Add Razorpay Checkout to Oracle APEX</strong></p>
<p>Page Attributes → JavaScript File URLs:</p>
<pre><code class="language-plaintext">https://checkout.razorpay.com/v1/checkout.js
</code></pre>
<h3>Step 12: Create Order From Oracle APEX</h3>
<p>Example JavaScript:</p>
<pre><code class="language-javascript">fetch( 'https://your-backend.onrender.com/create-order', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 1000, booking_ref: 'BOOK001' }) } ) .then(res =&gt; res.json()) .then(data =&gt; { var options = { key: 'YOUR_KEY_ID', amount: data.amount, currency: 'INR', order_id: data.order_id }; new Razorpay(options).open(); });
</code></pre>
<h3>Step 13: Test Using Razorpay Test Cards</h3>
<p>Use Razorpay test credentials:</p>
<pre><code class="language-plaintext">Card Number: 4111 1111 1111 1111
Expiry Date: Any future date
CVV: 123
OTP: 123456
</code></pre>
<h3>Security Considerations</h3>
<p>Never expose:</p>
<ul>
<li><p>Razorpay Secret Key</p>
</li>
<li><p>Webhook Secret</p>
</li>
<li><p>API Credentials</p>
</li>
</ul>
<p>Only the backend should access these values.</p>
<h3><strong>Final Result</strong></h3>
<p>By the end of this implementation you have:</p>
<ul>
<li><p>Oracle APEX frontend</p>
</li>
<li><p>Secure Node.js backend</p>
</li>
<li><p>Free cloud deployment</p>
</li>
<li><p>Real Razorpay order creation</p>
</li>
<li><p>Test payments working end-to-end</p>
</li>
</ul>
<p>This architecture scales well from prototypes to production systems and follows standard payment security practices.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a5086171f31058cc5920051/31bc3b78-173b-4e13-a1ff-8425c018127a.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6a5086171f31058cc5920051/f2013c87-486d-407f-a13a-069741fcbaaa.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6a5086171f31058cc5920051/153e0e0b-d970-4831-8caf-e8e1277290e7.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6a5086171f31058cc5920051/487322a4-cb4b-4b94-880e-4a083603ad02.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Thank You for your time.</strong></p>
<p>If you need any help or have any questions, feel free to leave a comment or contact me via email.</p>
<p><strong>Email:</strong> <a href="mailto:arfaats999@gmail.com">arfaats999@gmail.com</a></p>
]]></content:encoded></item><item><title><![CDATA[Face Detection Login System]]></title><description><![CDATA[Learn how to build a secure AI-powered face recognition login system in Oracle APEX using face-api.js and custom authentication.]]></description><link>https://apexcraftsbyarfaat.hashnode.dev/face-detection-login-system</link><guid isPermaLink="true">https://apexcraftsbyarfaat.hashnode.dev/face-detection-login-system</guid><category><![CDATA[Oracle]]></category><category><![CDATA[#oracle-apex]]></category><category><![CDATA[FaceRecognition]]></category><category><![CDATA[Model]]></category><category><![CDATA[Oracle APEX Custom authentication]]></category><category><![CDATA[Oracle Database]]></category><dc:creator><![CDATA[Arfaat Shaikh]]></dc:creator><pubDate>Fri, 10 Jul 2026 10:35:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a5086171f31058cc5920051/a1c3aa08-b7ee-4143-b80b-74c5a1878c4e.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This document covers the complete implementation of a biometric face detection login system built inside Oracle APEX 24.2. The system replaces or supplements the standard username/password login with a real-time webcam-based face recognition flow, powered entirely by face-api.js running in the browser — no external paid API required.</p>
<h2>1.1 Technology Stack</h2>
<table style="min-width:50px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><td><p><strong>Component</strong></p></td><td><p><strong>Detail</strong></p></td></tr><tr><td><p><strong>UI Platform</strong></p></td><td><p>Oracle APEX 24.2</p></td></tr><tr><td><p><strong>Database</strong></p></td><td><p>Oracle Database 19c</p></td></tr><tr><td><p><strong>Face AI Library</strong></p></td><td><p>face-api.js v0.22.2 (browser-side)</p></td></tr><tr><td><p><strong>Face Models</strong></p></td><td><p>TinyFaceDetector + FaceLandmark68 + FaceRecognition</p></td></tr><tr><td><p><strong>Authentication</strong></p></td><td><p>Custom APEX Auth Scheme (PL/SQL function)</p></td></tr><tr><td><p><strong>Data Storage</strong></p></td><td><p>apex_face_users table (CLOB descriptor)</p></td></tr><tr><td><p><strong>Login Page</strong></p></td><td><p>Page 9999 (APEX default login page)</p></td></tr><tr><td><p><strong>App URL</strong></p></td><td><p><a target="_self" rel="noopener noreferrer nofollow" class="cursor-pointer" href="https://oracleapex.com/ords/f?p=218621:LOGIN" style="pointer-events:none">https://oracleapex.com/ords/f?p=218621:LOGIN</a></p></td></tr></tbody></table>

<h2>1.2 How It Works — Flow Summary</h2>
<ul>
<li>User opens the APEX login page (Page 9999)</li>
<li>Two buttons are visible: <strong>Register Your Face</strong> and <strong>Login with Face Detection</strong></li>
<li>Registration: user enters username → camera opens → face is auto-captured after 5 confirmed frames → 128-point descriptor saved to DB</li>
<li>Login: camera opens → face scanned → descriptor compared against DB via Euclidean distance → if match ≤ 0.5 threshold → login submitted automatically</li>
<li>APEX custom auth scheme validates the bypass password and confirms username exists in <code>apex_face_users</code></li>
</ul>
<h2>2. Database Setup</h2>
<h3>2.1 Face Users Table</h3>
<p><strong>Run this SQL in SQL Workshop → SQL Commands:</strong></p>
<p>CREATE TABLE apex_face_users (<br />id   NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,<br />username  VARCHAR2(100) NOT NULL,<br />face_descriptor  CLOB NOT NULL,<br />created_date     DATE DEFAULT SYSDATE<br />);</p>
<p>NOTE: face_descriptor stores a 128-element JSON float array like [0.12, -0.34, 0.56, ...]. It is NOT an image — do not use a file upload control to populate it.</p>
<h3>2.2 Custom Authentication Function</h3>
<p>Run this in SQL Workshop → SQL Commands:</p>
<p>CREATE OR REPLACE FUNCTION custom_face_auth ( p_username IN VARCHAR2, p_password IN VARCHAR2 ) RETURN BOOLEAN IS l_count NUMBER; BEGIN IF p_password = 'FACE_BYPASS_2024' THEN SELECT COUNT(*) INTO l_count FROM apex_face_users WHERE UPPER(username) = UPPER(p_username); RETURN l_count &gt; 0; END IF; RETURN FALSE; END;</p>
<h3><strong>3. APEX Authentication Scheme</strong></h3>
<p>Go to: Shared Components → Authentication Schemes → Create → From Scratch</p>
<table style="min-width:50px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><td><p><strong>Setting</strong></p></td><td><p><strong>Value</strong></p></td></tr><tr><td><p><strong>Name</strong></p></td><td><p>Face Detection Auth</p></td></tr><tr><td><p><strong>Scheme Type</strong></p></td><td><p>Custom</p></td></tr><tr><td><p><strong>Authentication Function</strong></p></td><td><p>custom_face_auth</p></td></tr><tr><td><p>After creating, make this scheme Current by clicking "Make Current Scheme" so APEX uses it for all logins.</p></td><td><p> </p></td></tr></tbody></table>

<h2>4. Model Files Setup</h2>
<h3>4.1 Download from GitHub</h3>
<p>Download all 6 files from: <a href="https://github.com/justadudewhohacks/face-api.js/tree/master/weights">https://github.com/justadudewhohacks/face-api.js/tree/master/weights</a></p>
<p>● tiny_face_detector_model-shard1<br />● tiny_face_detector_model-weights_manifest.json<br />● face_landmark_68_model-shard1<br />● face_landmark_68_model-weights_manifest.json<br />● face_recognition_model-shard1<br />● face_recognition_model-shard2 ← critical, often missed<br />● face_recognition_model-weights_manifest.json</p>
<h3>4.2 Upload to APEX</h3>
<p>Go to: Shared Components → Static Application Files → Upload</p>
<p>● Create a folder called models/<br />● Upload all 7 files into the models/ folder<br />● Reference path: #APP_FILES#models/</p>
<p><strong>IMPORTANT: face_recognition_model has TWO shards (shard1 and shard2). Missing shard2 causes a tensor shape error and models will not load</strong>.</p>
<h2>5. APEX Page 9999 Setup</h2>
<h3>5.1 Existing Items (Pre-built by APEX)</h3>
<table style="min-width:50px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><td><p><strong>Item</strong></p></td><td><p><strong>Type</strong></p></td></tr><tr><td><p><strong>Region</strong></p></td><td><p>Face Detection (Content Body)</p></td></tr><tr><td><p><strong>P9999_USERNAME</strong></p></td><td><p>Text Field</p></td></tr><tr><td><p><strong>P9999_PASSWORD</strong></p></td><td><p>Text Field (Password)</p></td></tr><tr><td><p><strong>P9999_REMEMBER</strong></p></td><td><p>Checkbox</p></td></tr><tr><td><p><strong>LOGIN button</strong></p></td><td><p>Submit → request = LOGIN</p></td></tr></tbody></table>

<h2>5.2 New Buttons to Create</h2>
<table style="min-width:50px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><td><p><strong>Button Name</strong></p></td><td><p><strong>Configuration</strong></p></td></tr><tr><td><p><strong>Login with Face Detection</strong></p></td><td><p>Action: Defined by Dynamic Action → DA calls openFaceDetection()</p></td></tr><tr><td><p><strong>Register Your Face</strong></p></td><td><p>Action: Defined by Dynamic Action → DA calls openRegisterModal()</p></td></tr></tbody></table>

<h2>5.3 Dynamic Actions</h2>
<p>For each button create a Dynamic Action:</p>
<p>●        Event: Click<br />●        Selection Type: Button → select the button<br />●        TRUE Action: Execute JavaScript Code<br />○        Login button → Code: openFaceDetection()<br />○        Register button → Code: openRegisterModal()<br />Fire on Page Load: NO</p>
<h2>5.4 HTML Regions to Create</h2>
<table style="min-width:50px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><td><p><strong>Region Name</strong></p></td><td><p><strong>Configuration</strong></p></td></tr><tr><td><p><strong>Registration Modal</strong></p></td><td><p>Type: Static Content | Position: Content Body | Paste registration HTML</p></td></tr><tr><td><p><strong>Verification Overlay</strong></p></td><td><p>Type: Static Content | Position: Content Body | Paste verification HTML</p></td></tr></tbody></table>

<h2>6. AJAX Callback Processes</h2>
<p>Go to: Page 9999 → Processing → right-click → Create Process → AJAX Callback</p>
<h3>6.1 REGISTER_FACE - Saves a new face descriptor or updates existing one:</h3>
<p>DECLARE l_username VARCHAR2(100) := apex_application.g_x01; l_descriptor CLOB := apex_application.g_x02; l_count NUMBER; BEGIN SELECT COUNT(*) INTO l_count FROM apex_face_users WHERE username = l_username; IF l_count &gt; 0 THEN UPDATE apex_face_users SET face_descriptor = l_descriptor, created_date = SYSDATE WHERE username = l_username; ELSE INSERT INTO apex_face_users (username, face_descriptor) VALUES (l_username, l_descriptor); END IF; COMMIT; htp.p('{"status":"success","username":"' || l_username || '"}' ); EXCEPTION WHEN OTHERS THEN htp.p('{"status":"error","message":"' || SQLERRM || '"}' ); END;</p>
<h3>6.2 VERIFY_FACE Returns all stored face descriptors for JS-side Euclidean comparison:</h3>
<p>DECLARE l_json CLOB := '['; l_first BOOLEAN := TRUE; BEGIN FOR r IN (SELECT username, face_descriptor FROM apex_face_users) LOOP IF NOT l_first THEN l_json := l_json || ','; END IF; l_json := l_json || '{"username":"' || r.username || '","descriptor":' || r.face_descriptor || '}'; l_first := FALSE; END LOOP; l_json := l_json || ']'; apex_util.set_session_state('P9999_STATUS', l_json); htp.p(l_json); END;</p>
<h3>6.3 DO_FACE_LOGIN Sets session state before page submit:</h3>
<p>BEGIN apex_util.set_session_state('P9999_USERNAME', apex_application.g_x01); htp.p('{"status":"ok"}'); END;</p>
<h2>7. JavaScript — Execute when Page Loads</h2>
<h3>Paste the entire block below into: Page 9999 → JavaScript → Execute when Page Loads</h3>
<h3>7.1 Model Loading</h3>
<p>const MODEL_URL = '<a href="https://oracleapex.com/ords/r/unionsys/face-detection/files/static/v1/models">https://oracleapex.com/ords/r/unionsys/face-detection/files/static/v1/models</a>'; let cameraStream = null, detectionInterval = null, modelsLoaded = false; const faceScript = document.createElement('script'); faceScript.src = '<a href="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js">https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js</a>'; faceScript.onload = async function () { await faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL); await faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL); await faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL); modelsLoaded = true; console.log('Face models loaded.'); }; document.head.appendChild(faceScript);</p>
<h3>7.2 Key Fix — Landmark Accuracy The most important fix for accurate face landmark rendering (eyes, lips, nose in correct position): // After video loads, match canvas dimensions to display size</h3>
<p>var displaySize = { width: video.offsetWidth, height: video.offsetHeight }; faceapi.matchDimensions(canvas, displaySize);</p>
<h3>// Before drawing detections, results to display size //</h3>
<p>var resized = faceapi.resizeResults(det, displaySize); faceapi.draw.drawDetections(canvas, resized); faceapi.draw.drawFaceLandmarks(canvas, resized);</p>
<h3>7.3 Login Flow On successful face match, the JS sets the APEX items and submits the login:</h3>
<p>apex.item('P9999_USERNAME').setValue(username); apex.item('P9999_PASSWORD').setValue('FACE_BYPASS_2024'); apex.page.submit({ request: 'LOGIN' }); The custom_face_auth function then checks apex_face_users and returns TRUE if username exists.</p>
<h2>8. Bugs Fixed Along the Way</h2>
<table>
<thead>
<tr>
<th><strong>Bug</strong></th>
<th><strong>Fix</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Page submitting on button click</td>
<td>HTML buttons default to <code>type="submit"</code> — fixed by adding <code>type="button"</code> to all buttons in HTML regions</td>
</tr>
<tr>
<td><code>openFaceDetection</code> is not defined</td>
<td>Functions declared inside <code>onload</code> callback were not global — fixed by changing all to <code>window.functionName = function()</code></td>
</tr>
<tr>
<td><code>face_recognition_model</code> 404 error</td>
<td><code>shard2</code> file was missing — fixed by downloading and uploading <code>face_recognition_model-shard2</code></td>
</tr>
<tr>
<td>Tensor shape mismatch error</td>
<td>Caused by the missing <code>shard2</code> — resolved when <code>shard2</code> was uploaded</td>
</tr>
<tr>
<td>Landmarks in wrong position</td>
<td>Canvas native size did not match display size — fixed using <code>faceapi.matchDimensions</code> + <code>faceapi.resizeResults</code></td>
</tr>
<tr>
<td>Face captured but not saving to DB</td>
<td>Button without <code>type="button"</code> was submitting the page before AJAX completed</td>
</tr>
<tr>
<td>Login not working after face match</td>
<td><code>DO_FACE_LOGIN</code> alone does not create session — fixed by submitting the <code>LOGIN</code> request with <code>FACE_BYPASS_2024</code></td>
</tr>
</tbody></table>
<h2>9. Complete End-to-End Flow</h2>
<h3>9.1 Registration Flow</h3>
<p>●        User clicks Register Your Face button<br />●        openRegisterModal() fires via Dynamic Action<br />●        Modal opens — Step 1 shown: username input + Continue button<br />●        User enters username and clicks Continue<br />●        startRegisterCamera() fires — webcam starts<br />●        Step 2 shown: ring with live video feed<br />●        faceapi detects face every 200ms using TinyFaceDetector<br />●        faceapi.matchDimensions ensures landmarks render accurately<br />●        5 confirmed frames fill the progress dots<br />●        AUTO CAPTURE fires — no button click needed<br />●        Ring turns green with pulsing glow<br />●        REGISTER_FACE AJAX process called with username (x01) + descriptor JSON (x02)<br />●        PL/SQL inserts or updates apex_face_users table<br />●        Success message shown, modal closes after 2 seconds</p>
<p>9.2 Login Flow</p>
<p>● User clicks Login with Face Detection button<br />● openFaceDetection() fires via Dynamic Action<br />● Verification overlay opens with violet/indigo pulsing ring<br />● Webcam starts, faceapi runs every 200ms<br />● 5 confirmed frames fill progress dots (amber color)<br />● VERIFY_FACE AJAX called — PL/SQL returns all stored descriptors as JSON<br />● JS loops through stored descriptors and calculates Euclidean distance<br />● Best match checked against threshold 0.5<br />● Match found → ring turns green → Welcome message shown<br />● After 1.8s: P9999_USERNAME = matched username, P9999_PASSWORD = FACE_BYPASS_2024, page submitted<br />● custom_face_auth function runs → checks apex_face_users → returns TRUE → user logged in<br />● APEX redirects to home page<br />● No match → ring turns red → Access Denied → overlay closes after 3 seconds</p>
<h2>10. Troubleshooting Quick Reference</h2>
<table>
<thead>
<tr>
<th><strong>Symptom</strong></th>
<th><strong>Solution</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Models not loading</td>
<td>Check F12 console for 404 — verify model URL matches static file path in Shared Components</td>
</tr>
<tr>
<td>Button does nothing</td>
<td>Check Dynamic Action → TRUE Action is Execute JavaScript Code with correct function name</td>
</tr>
<tr>
<td>Page jumps back on camera open</td>
<td>Button inside HTML region is missing <code>type="button"</code></td>
</tr>
<tr>
<td>Face not detected</td>
<td>Improve lighting, move closer, use Chrome browser</td>
</tr>
<tr>
<td>Always says Access Denied</td>
<td>No data in <code>apex_face_users</code> — register a face first</td>
</tr>
<tr>
<td>Login fails after green ring</td>
<td>Verify <code>custom_face_auth</code> function exists in DB and Authentication Scheme is set to Current</td>
</tr>
<tr>
<td>Landmarks shifted/offset</td>
<td><code>faceapi.matchDimensions</code> or <code>faceapi.resizeResults</code> missing in JS</td>
</tr>
<tr>
<td><code>shard2</code> 404 error</td>
<td>Upload <code>face_recognition_model-shard2</code> to <strong>Static Application Files → models/</strong></td>
</tr>
</tbody></table>
<h2>11. Security Notes</h2>
<p>The FACE_BYPASS_2024 password is a shared secret used to bypass normal password auth. For production use, consider rotating this value and storing it in APEX Application Settings rather than hardcoding in JS.</p>
<p>● Face descriptors are stored as plain JSON in CLOB — consider encrypting at rest for production<br />● Euclidean threshold of 0.5 balances accuracy vs security — lower = stricter<br />● For multi-user environments, register multiple face angles per user for better accuracy<br />● HTTPS is required for webcam access in all modern browsers<br />● This implementation is suitable for internal tools and demos — for banking/high security apps, use a dedicated biometric SDK</p>
<p><strong>Thank You for your time.</strong></p>
<p>If you need any help or have any questions, feel free to leave a comment or contact me via email.</p>
<p><strong>Email:</strong> <a href="mailto:arfaats999@gmail.com">arfaats999@gmail.com</a></p>
]]></content:encoded></item></channel></rss>