Face Detection Login System
Oracle APEX 24.2 · Complete Technical Documentation

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.
1.1 Technology Stack
Component | Detail |
UI Platform | Oracle APEX 24.2 |
Database | Oracle Database 19c |
Face AI Library | face-api.js v0.22.2 (browser-side) |
Face Models | TinyFaceDetector + FaceLandmark68 + FaceRecognition |
Authentication | Custom APEX Auth Scheme (PL/SQL function) |
Data Storage | apex_face_users table (CLOB descriptor) |
Login Page | Page 9999 (APEX default login page) |
App URL |
1.2 How It Works — Flow Summary
- User opens the APEX login page (Page 9999)
- Two buttons are visible: Register Your Face and Login with Face Detection
- Registration: user enters username → camera opens → face is auto-captured after 5 confirmed frames → 128-point descriptor saved to DB
- Login: camera opens → face scanned → descriptor compared against DB via Euclidean distance → if match ≤ 0.5 threshold → login submitted automatically
- APEX custom auth scheme validates the bypass password and confirms username exists in
apex_face_users
2. Database Setup
2.1 Face Users Table
Run this SQL in SQL Workshop → SQL Commands:
CREATE TABLE apex_face_users (
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR2(100) NOT NULL,
face_descriptor CLOB NOT NULL,
created_date DATE DEFAULT SYSDATE
);
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.
2.2 Custom Authentication Function
Run this in SQL Workshop → SQL Commands:
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 > 0; END IF; RETURN FALSE; END;
3. APEX Authentication Scheme
Go to: Shared Components → Authentication Schemes → Create → From Scratch
Setting | Value |
Name | Face Detection Auth |
Scheme Type | Custom |
Authentication Function | custom_face_auth |
After creating, make this scheme Current by clicking "Make Current Scheme" so APEX uses it for all logins. |
|
4. Model Files Setup
4.1 Download from GitHub
Download all 6 files from: https://github.com/justadudewhohacks/face-api.js/tree/master/weights
● tiny_face_detector_model-shard1
● tiny_face_detector_model-weights_manifest.json
● face_landmark_68_model-shard1
● face_landmark_68_model-weights_manifest.json
● face_recognition_model-shard1
● face_recognition_model-shard2 ← critical, often missed
● face_recognition_model-weights_manifest.json
4.2 Upload to APEX
Go to: Shared Components → Static Application Files → Upload
● Create a folder called models/
● Upload all 7 files into the models/ folder
● Reference path: #APP_FILES#models/
IMPORTANT: face_recognition_model has TWO shards (shard1 and shard2). Missing shard2 causes a tensor shape error and models will not load.
5. APEX Page 9999 Setup
5.1 Existing Items (Pre-built by APEX)
Item | Type |
Region | Face Detection (Content Body) |
P9999_USERNAME | Text Field |
P9999_PASSWORD | Text Field (Password) |
P9999_REMEMBER | Checkbox |
LOGIN button | Submit → request = LOGIN |
5.2 New Buttons to Create
Button Name | Configuration |
Login with Face Detection | Action: Defined by Dynamic Action → DA calls openFaceDetection() |
Register Your Face | Action: Defined by Dynamic Action → DA calls openRegisterModal() |
5.3 Dynamic Actions
For each button create a Dynamic Action:
● Event: Click
● Selection Type: Button → select the button
● TRUE Action: Execute JavaScript Code
○ Login button → Code: openFaceDetection()
○ Register button → Code: openRegisterModal()
Fire on Page Load: NO
5.4 HTML Regions to Create
Region Name | Configuration |
Registration Modal | Type: Static Content | Position: Content Body | Paste registration HTML |
Verification Overlay | Type: Static Content | Position: Content Body | Paste verification HTML |
6. AJAX Callback Processes
Go to: Page 9999 → Processing → right-click → Create Process → AJAX Callback
6.1 REGISTER_FACE - Saves a new face descriptor or updates existing one:
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 > 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;
6.2 VERIFY_FACE Returns all stored face descriptors for JS-side Euclidean comparison:
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;
6.3 DO_FACE_LOGIN Sets session state before page submit:
BEGIN apex_util.set_session_state('P9999_USERNAME', apex_application.g_x01); htp.p('{"status":"ok"}'); END;
7. JavaScript — Execute when Page Loads
Paste the entire block below into: Page 9999 → JavaScript → Execute when Page Loads
7.1 Model Loading
const MODEL_URL = 'https://oracleapex.com/ords/r/unionsys/face-detection/files/static/v1/models'; let cameraStream = null, detectionInterval = null, modelsLoaded = false; const faceScript = document.createElement('script'); faceScript.src = 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js'; 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);
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
var displaySize = { width: video.offsetWidth, height: video.offsetHeight }; faceapi.matchDimensions(canvas, displaySize);
// Before drawing detections, results to display size //
var resized = faceapi.resizeResults(det, displaySize); faceapi.draw.drawDetections(canvas, resized); faceapi.draw.drawFaceLandmarks(canvas, resized);
7.3 Login Flow On successful face match, the JS sets the APEX items and submits the login:
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.
8. Bugs Fixed Along the Way
| Bug | Fix |
|---|---|
| Page submitting on button click | HTML buttons default to type="submit" — fixed by adding type="button" to all buttons in HTML regions |
openFaceDetection is not defined |
Functions declared inside onload callback were not global — fixed by changing all to window.functionName = function() |
face_recognition_model 404 error |
shard2 file was missing — fixed by downloading and uploading face_recognition_model-shard2 |
| Tensor shape mismatch error | Caused by the missing shard2 — resolved when shard2 was uploaded |
| Landmarks in wrong position | Canvas native size did not match display size — fixed using faceapi.matchDimensions + faceapi.resizeResults |
| Face captured but not saving to DB | Button without type="button" was submitting the page before AJAX completed |
| Login not working after face match | DO_FACE_LOGIN alone does not create session — fixed by submitting the LOGIN request with FACE_BYPASS_2024 |
9. Complete End-to-End Flow
9.1 Registration Flow
● User clicks Register Your Face button
● openRegisterModal() fires via Dynamic Action
● Modal opens — Step 1 shown: username input + Continue button
● User enters username and clicks Continue
● startRegisterCamera() fires — webcam starts
● Step 2 shown: ring with live video feed
● faceapi detects face every 200ms using TinyFaceDetector
● faceapi.matchDimensions ensures landmarks render accurately
● 5 confirmed frames fill the progress dots
● AUTO CAPTURE fires — no button click needed
● Ring turns green with pulsing glow
● REGISTER_FACE AJAX process called with username (x01) + descriptor JSON (x02)
● PL/SQL inserts or updates apex_face_users table
● Success message shown, modal closes after 2 seconds
9.2 Login Flow
● User clicks Login with Face Detection button
● openFaceDetection() fires via Dynamic Action
● Verification overlay opens with violet/indigo pulsing ring
● Webcam starts, faceapi runs every 200ms
● 5 confirmed frames fill progress dots (amber color)
● VERIFY_FACE AJAX called — PL/SQL returns all stored descriptors as JSON
● JS loops through stored descriptors and calculates Euclidean distance
● Best match checked against threshold 0.5
● Match found → ring turns green → Welcome message shown
● After 1.8s: P9999_USERNAME = matched username, P9999_PASSWORD = FACE_BYPASS_2024, page submitted
● custom_face_auth function runs → checks apex_face_users → returns TRUE → user logged in
● APEX redirects to home page
● No match → ring turns red → Access Denied → overlay closes after 3 seconds
10. Troubleshooting Quick Reference
| Symptom | Solution |
|---|---|
| Models not loading | Check F12 console for 404 — verify model URL matches static file path in Shared Components |
| Button does nothing | Check Dynamic Action → TRUE Action is Execute JavaScript Code with correct function name |
| Page jumps back on camera open | Button inside HTML region is missing type="button" |
| Face not detected | Improve lighting, move closer, use Chrome browser |
| Always says Access Denied | No data in apex_face_users — register a face first |
| Login fails after green ring | Verify custom_face_auth function exists in DB and Authentication Scheme is set to Current |
| Landmarks shifted/offset | faceapi.matchDimensions or faceapi.resizeResults missing in JS |
shard2 404 error |
Upload face_recognition_model-shard2 to Static Application Files → models/ |
11. Security Notes
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.
● Face descriptors are stored as plain JSON in CLOB — consider encrypting at rest for production
● Euclidean threshold of 0.5 balances accuracy vs security — lower = stricter
● For multi-user environments, register multiple face angles per user for better accuracy
● HTTPS is required for webcam access in all modern browsers
● This implementation is suitable for internal tools and demos — for banking/high security apps, use a dedicated biometric SDK
Thank You for your time.
If you need any help or have any questions, feel free to leave a comment or contact me via email.
Email: arfaats999@gmail.com
About the author
- Hi, I'm Arfaat Shaikh, an Oracle APEX Developer with a passion for building scalable, user-friendly enterprise applications. I enjoy solving real-world problems using Oracle APEX, PL/SQL, SQL, REST APIs, and AI-powered solutions. Through this blog, I document practical Oracle APEX tutorials, production-ready solutions, troubleshooting guides, and best practices from my development journey. My goal is to make complex concepts simple and create resources that help developers learn faster and build with confidence. Whether you're just getting started with Oracle APEX or looking for advanced techniques, I hope you'll find something useful here. Happy coding!
Comments
Join the discussionNo comments yet. Be the first to comment.




