Skip to main content

Zero-Plugin Barcode & QR Scanner in Oracle APEX

I built a barcode scanner for Oracle APEX without installing a single plugin

No native extensions, no paid plugin, just the browser's camera and a bit of JavaScript. Here's the whole thing, step by step.

I built this for a check-in system where scanning tickets manually was too slow. No plugin, no paid SDK just the browser's camera, a bit of JavaScript, and PL/SQL on the APEX side.

What you'll end up with: tap "Start Scanner", point the phone at a QR code, and the person's name shows up on screen as checked in instantly, straight from the database.
1

Set up the page and the camera window

Blank APEX page (mine is page 17). Add a static content region with a video tag that's where the camera feed shows up.

HTML Static Content Region
<div id="viewfinder-container" style="text-align: center; max-width: 500px; margin: 0 auto;">
    <video id="barcode-video" autoplay playsinline style="width: 100%; border-radius: 8px; border: 2px solid #0076df; background: #000;"></video>
    <div id="scanner-controls" style="margin-top: 12px;">
        <button id="btn-toggle-camera" type="button" class="t-Button t-Button--hot t-Button--icon t-Button--iconLeft">
            <span class="t-Icon fa fa-camera" aria-hidden="true"></span>Start Scanner
        </button>
    </div>
</div>

Add three display-only items below it: P17_SCANNED_CODE, P17_ATTENDEE_NAME, P17_STATUS.

2

Add a fallback for iPhones

Chrome on Android reads barcodes natively, but iPhone Safari doesn't. ZXing.js fills that gap it only kicks in when native support is missing.

Page Properties → JavaScript → File URLs:

Page Properties → File URLs
https://unpkg.com/@zxing/library@latest/umd/index.min.js
3

Write the PL/SQL that checks people in

The scanned code gets sent here through AJAX. It looks up the ticket, checks if the person's already inside, and either checks them in or warns you.

Processing → AJAX Callbacks → new process named PROCESS_SCAN:

PL/SQL PROCESS_SCAN
declare
    l_code          varchar2(255) := apex_application.g_x01; -- Code passed from JS
    l_attendee_name varchar2(150);
    l_already_in    varchar2(1);
begin
    -- Look up attendee or inventory item
    select full_name, checked_in_flag
      into l_attendee_name, l_already_in
      from event_attendees
     where ticket_qr_code = l_code;

    if l_already_in = 'Y' then
        apex_json.open_object;
        apex_json.write('success', false);
        apex_json.write('message', 'Warning: ' || l_attendee_name || ' is already checked in!');
        apex_json.close_object;
    else
        -- Update database record
        update event_attendees
           set checked_in_flag = 'Y',
               check_in_time   = systimestamp
         where ticket_qr_code = l_code;

        -- Return JSON success response
        apex_json.open_object;
        apex_json.write('success', true);
        apex_json.write('message', 'Checked in: ' || l_attendee_name);
        apex_json.write('attendee_name', l_attendee_name);
        apex_json.write('status', 'CONFIRMED');
        apex_json.close_object;
    end if;

exception
    when no_data_found then
        apex_json.open_object;
        apex_json.write('success', false);
        apex_json.write('message', 'Invalid Code: Ticket not found.');
        apex_json.close_object;
    when others then
        apex_json.open_object;
        apex_json.write('success', false);
        apex_json.write('message', 'Error: ' || sqlerrm);
        apex_json.close_object;
end;

The table behind it, just five columns:

SQL event_attendees table
create table event_attendees (
    attendee_id      number generated by default as identity primary key,
    full_name        varchar2(150) not null,
    ticket_qr_code   varchar2(255) unique not null,
    checked_in_flag  varchar2(1) default 'N',
    check_in_time    timestamp
);
4

Build the scanner engine in JavaScript

Turns the camera on, watches for a barcode, and the moment it finds one, pauses the camera and calls PROCESS_SCAN. A cooldown stops the same code firing twice.

Page Properties → JavaScript → Function and Global Variable Declaration:

JavaScript ApexScanner engine
var ApexScanner = {
    videoElement: null,
    stream: null,
    isScanning: false,
    cooldown: false,
    lastScannedCode: null,
    detector: null,

    start: async function() {
        this.videoElement = document.getElementById('barcode-video');
        
        try {
            this.stream = await navigator.mediaDevices.getUserMedia({
                video: { facingMode: 'environment' }
            });
            this.videoElement.srcObject = this.stream;
            this.isScanning = true;
            this.cooldown = false;

            if ('BarcodeDetector' in window) {
                this.detector = new BarcodeDetector({
                    formats: ['qr_code', 'code_128', 'ean_13', 'upc_a']
                });
                this.scanNative();
            } else {
                this.scanZXing();
            }
        } catch (err) {
            apex.message.showErrors([{
                type: 'error',
                location: 'page',
                message: 'Unable to access camera: ' + err.message,
                unsafe: false
            }]);
        }
    },

    stop: function() {
        if (this.stream) {
            this.stream.getTracks().forEach(track => track.stop());
        }
        this.isScanning = false;
        
        // Reset button UI
        const btn = document.getElementById('btn-toggle-camera');
        if (btn) {
            btn.innerHTML = '<span class="t-Icon fa fa-camera"></span> Scan Next Code';
        }
    },

    scanNative: async function() {
        if (!this.isScanning) return;

        try {
            const barcodes = await this.detector.detect(this.videoElement);
            if (barcodes.length > 0 && !this.cooldown) {
                const code = barcodes[0].rawValue;
                // Don't re-trigger for the exact same code
                if (code !== this.lastScannedCode) {
                    this.handleCodeFound(code);
                }
            }
        } catch (e) {}

        if (this.isScanning) {
            requestAnimationFrame(() => this.scanNative());
        }
    },

    scanZXing: function() {
        const codeReader = new ZXing.BrowserMultiFormatReader();
        codeReader.decodeFromVideoDevice(null, 'barcode-video', (result, err) => {
            if (result && !this.cooldown && this.isScanning) {
                if (result.text !== this.lastScannedCode) {
                    this.handleCodeFound(result.text);
                }
            }
        });
    },

    handleCodeFound: function(codeValue) {
        this.cooldown = true;
        this.lastScannedCode = codeValue;

        // 1. AUTO-PAUSE CAMERA IMMEDIATELY ON DETECT
        this.stop(); 

        // 2. Set item value
        apex.item('P17_SCANNED_CODE').setValue(codeValue);

        // 3. Fire AJAX Lookup
        apex.server.process(
            'PROCESS_SCAN',
            { x01: codeValue },
            {
                dataType: 'json',
                success: (pData) => {
                    if (pData.success) {
                        apex.message.showPageSuccess(pData.message);
                        apex.item('P17_ATTENDEE_NAME').setValue(pData.attendee_name);
                        apex.item('P17_STATUS').setValue(pData.status);
                    } else {
                        apex.message.showErrors([{
                            type: 'error',
                            location: 'page',
                            message: pData.message,
                            unsafe: false
                        }]);
                    }
                }
            }
        );
    }
};
Heads-up: match P17_... / P17_... to your actual page number, or the values won't show up.
5

Hook up the button

Listens for the button click and toggles the camera on/off, swapping the button text to match.

Page Properties → JavaScript → Execute when Page Loads:

JavaScript Execute on Page Load
// Toggle Scanner Button Listener
document.getElementById('btn-toggle-camera').addEventListener('click', function() {
    if (ApexScanner.isScanning) {
        ApexScanner.stop();
        this.innerHTML = '<span class="t-Icon fa fa-camera"></span> Start Scanner';
    } else {
        ApexScanner.start();
        this.innerHTML = '<span class="t-Icon fa fa-stop"></span> Stop Scanner';
    }
});
Table columns, quick reference:
ColumnWhat it stores
attendee_idAuto-generated unique ID for each attendee
full_nameThe attendee's name
ticket_qr_codeThe unique code printed on their ticket
checked_in_flagY or N, whether they've already entered
check_in_timeTimestamp of when they checked in
Thanks for reading if you build this, I'd love to hear how it goes.

Comments

Popular posts from this blog

Generate Custom PDFs in Oracle APEX with jsPDF

Generate Custom PDFs in Oracle APEX with jsPDF A complete step-by-step guide to building a client-side PDF generator with profile image embedding and PL/SQL database persistence. &#128196; Step-by-step guide Oracle APEX jsPDF Dynamic Actions PL/SQL JavaScript "Picture this: your Oracle APEX application is polished, your users love it — but when they ask for a downloadable report with a profile picture and custom styling, you realize vanilla APEX isn't built for that out of the box. What if a single JavaScript library and one Dynamic Action could change everything?" Welcome to the world of jsPDF inside Oracle APEX . In this tutorial, we wire up a PDF generator that captures a user's name, email, phone number, and profile picture — renders them into a beautiful PDF layout — and saves everything to Oracle, all in one button click. ⓘ What you'll build: A dynamic PDF generat...

APEX Custom Auth Settings, Decoded!

Oracle APEX Security PL/SQL APEX Custom Auth Settings, Decoded Why I Build this I was building an internal APEX app — strictly for people on our corporate network. The default authentication worked, but it had a few things that kept bothering me. Anyone with valid credentials could log in from anywhere. Sessions expired with a useless error page. There was no audit trail — no way to know who logged in, when, or from where. And passwords were stored in plain text, which I just couldn't leave alone. So I did what any developer does — I built it myself. Custom authentication, from scratch, in PL/SQL. Turned out to be one of the best learning experiences I've had with APEX. Here's exactly how I did it. Img 1 : Head to Shared Components → Authentication Schemes → Create, choose Custom as the Scheme Type, and plug in your function and procedure names. Quick note before we get into the code — the s...

AI Agents in Oracle APEX - Part 2: Building the Live Demo

agent: online AI Agents in Oracle APEX — Part 2: Building the Live Demo The config, the tools, the button, and what to type at it. This picks up straight from Part 1 — no theory this time, just the build. I'm using two small tables, EMP_D and DEPT_D , so the classic schema stays out of the way of the actual point. By the end, you'll have an agent that looks up employees, summarizes salaries by department, hires someone new, and highlights a row on the page — all from a chat box. 01 Create the AI Agent Shared Components → Tasks → AI Agents → Create. First, APEX needs an AI Service configured — that's where your provider API key (OpenAI, Gemini, etc.) lives. Workspace Utilities → AI Services → Create Static ID + pick your model — I used command-a-03-2025 , fast and cheap enough to hammer with test prompts Paste your API key under Credentials — APEX auto-wraps it in a Web Credential, so it never sits exposed in page source Hit Test Connection be...