Skip to main content

Backing Up Oracle APEX Applications to GitHub Automatically (No External Tools Needed)

Backing Up Oracle APEX Applications to GitHub Automatically (No External Tools Needed)

Before every deployment, I want a safe copy of my APEX application stored somewhere I can always get back to. In this post I'll show how to push a full application export to a GitHub repository, with the date in the folder name, using only PL/SQL inside APEX. It works on the free apex.oracle.com service, so you can try it without installing anything.

What We Are Building

A button in an APEX page that does this:

  1. Exports your application using APEX_EXPORT
  2. Sends the file to GitHub through the GitHub REST API
  3. Stores it in a dated folder, like backups/2026-10-06_133022/

Every click creates a new restore point you can see in GitHub's history.

backups/
  2026-10-06_133022/
    f185224.sql              <- single-file export, restore with App Builder > Import
    split/f185224/...        <- optional, one file per page/component

What You Need

  • A GitHub account
  • An Oracle APEX workspace (apex.oracle.com is fine)

Part 1: GitHub Setup

Create the Repository

  1. Click + (top right) > New repository.
  2. Name it apex-backups.
  3. Choose Private, because application code can be sensitive.
  4. Tick Add a README file. This creates the main branch the API needs.
  5. Click Create repository.

Generate a Personal Access Token

APEX needs permission to write into your repo. A token is like a password limited to one job.

  1. Click your profile picture > Settings.
  2. Scroll down the left menu and click Developer settings.
  3. Click Personal access tokens > Fine-grained tokens > Generate new token.
  4. Set:
    • Token name: apex-backup
    • Expiration: 30 or 90 days
    • Repository access: Only select repositories > apex-backups
  5. Under Repository permissions, set Contents to Read and write.
  6. Click Generate token and copy it immediately. GitHub shows it only once. It starts with github_pat_.

Treat this token like a password. Never paste it into screenshots, chats or code.

Part 2: Store the Token in APEX Safely

Instead of hardcoding the token in PL/SQL, we store it in a Web Credential.

  1. In APEX go to App Builder > Workspace Utilities > Web Credentials > Create.
  2. Fill in:
    • Name: GitHub Token
    • Static ID: GITHUB_TOKEN
    • Authentication Type: HTTP Header
    • Credential Name or Header Name: Authorization
    • Credential Secret or Header Value: Bearer github_pat_xxxxxxxx
  3. Click Create.

Why the word "Bearer"?

Every protected API request carries a header like this:

Authorization: <scheme> <credentials>

Bearer is the scheme. It tells GitHub "whoever holds this token is allowed in." APEX sends the value exactly as typed, so if you leave out Bearer (or the single space after it), GitHub can't tell what kind of credential it received and answers 401 Unauthorized.

Part 3: Test the Connection First

Always test the connection before writing the full backup. In SQL Workshop > SQL Commands:

declare
  l_resp clob;
begin
  apex_web_service.clear_request_headers;
  apex_web_service.set_request_headers('User-Agent','apex-backup', p_reset => false);
  apex_web_service.set_request_headers('Accept','application/vnd.github+json', p_reset => false);

  l_resp := apex_web_service.make_rest_request(
    p_url                  => 'https://api.github.com/repos/<your-github-username>/apex-backups',
    p_http_method          => 'GET',
    p_credential_static_id => 'GITHUB_TOKEN');

  dbms_output.put_line('Status: ' || apex_web_service.g_status_code);
  dbms_output.put_line(substr(l_resp,1,300));
end;
/
Result Meaning
Status: 200 and JSON Connection works
401 Wrong token, or Bearer missing
404 Wrong username/repo name, or the token has no access to that repo
ORA-29273 / ORA-24247 / Certificate error The environment blocks outside calls. Use an Oracle Cloud Always Free Autonomous Database instead

Part 4: The Helper Procedure (Uploads One File)

This procedure sends any text content to a path in the repo. Run it once in SQL Commands.

create or replace procedure github_put_file(
  p_path    in varchar2,
  p_content in clob,
  p_message in varchar2
) is
  c_owner constant varchar2(100) := '<your-github-username>';
  c_repo  constant varchar2(100) := 'apex-backups';
  l_b64   clob;
  l_body  clob;
  l_resp  clob;
begin
  -- GitHub's API requires file content as Base64
  l_b64 := apex_web_service.blob2clobbase64(
             p_blob     => apex_util.clob_to_blob(p_content),
             p_newlines => 'NO');

  l_body := '{"message":"' || apex_escape.json(p_message) || '",'
         || '"content":"'  || l_b64 || '"}';

  apex_web_service.clear_request_headers;
  apex_web_service.set_request_headers('User-Agent','apex-backup', p_reset => false);
  apex_web_service.set_request_headers('Accept','application/vnd.github+json', p_reset => false);
  apex_web_service.set_request_headers('Content-Type','application/json', p_reset => false);

  l_resp := apex_web_service.make_rest_request(
    p_url => 'https://api.github.com/repos/' || c_owner || '/' || c_repo
             || '/contents/' || p_path,
    p_http_method          => 'PUT',
    p_body                 => l_body,
    p_credential_static_id => 'GITHUB_TOKEN');

  if apex_web_service.g_status_code not in (200,201) then
    raise_application_error(-20001,
      'GitHub upload failed for ' || p_path || ' (HTTP '
      || apex_web_service.g_status_code || '): ' || substr(l_resp,1,500));
  end if;
end;
/

Part 5: The Backup Procedure

create or replace procedure backup_app_to_github(
  p_app_id     in number,
  p_with_split in boolean default true
) is
  l_stamp  varchar2(30)  := to_char(sysdate,'YYYY-MM-DD_HH24MISS');
  l_root   varchar2(200) := 'backups/' || l_stamp;
  l_msg    varchar2(200) := 'APEX backup app ' || p_app_id || ' ' || l_stamp;
  l_files  apex_t_export_files;
  l_count  pls_integer := 0;
begin
  -- 1. Normal single-file export (same as App Builder > Export)
  l_files := apex_export.get_application(
               p_application_id          => p_app_id,
               p_split                   => false,
               p_with_date               => false,
               p_with_supporting_objects => 'Y');

  for i in 1 .. l_files.count loop
    github_put_file(l_root || '/' || l_files(i).name,
                    l_files(i).contents, l_msg);
    l_count := l_count + 1;
  end loop;

  -- 2. Optional split export: one file per page/component
  if p_with_split then
    l_files := apex_export.get_application(
                 p_application_id          => p_app_id,
                 p_split                   => true,
                 p_with_date               => false,
                 p_with_supporting_objects => 'Y');

    for i in 1 .. l_files.count loop
      github_put_file(l_root || '/split/f' || p_app_id || '/' || l_files(i).name,
                      l_files(i).contents, l_msg);
      l_count := l_count + 1;
    end loop;
  end if;

  dbms_output.put_line('Backup folder : ' || l_root);
  dbms_output.put_line('Files uploaded: ' || l_count);
end;
/

Run it to test:

begin
  backup_app_to_github(185224, p_with_split => false);  -- use your app ID
end;
/

Open your repo in GitHub and you should see a new dated folder with your export file.

Single file or split?

Feature Single File Split
Restore through App Builder > Import Yes No (needs SQLcl)
Readable page-by-page history in Git No Yes
Speed One API call Dozens of API calls

The single file is already a complete application backup. It contains pages, shared components, static files, plugin settings and supporting objects. I use p_with_split => false for routine pre-deployment backups and turn the split on only when I want to compare versions.

Part 6: Add a Button in Your APEX App

  1. Create a page with an item P4_APPLICATION (a select list of application IDs, or a number field).
  2. Add a button BACKUP with the action Submit Page.
  3. Under Processing, create a process of type Execute Code, When Button Pressed = BACKUP:
    begin
      backup_app_to_github(to_number(:P4_APPLICATION), p_with_split => false);
    end;
  4. Set the Success Message to Backup completed successfully.
  5. Important: Add a Branch after processing that redirects back to the same page when the button is pressed.

Security Checklist

  • Use a private repository.
  • Limit the token to one repo with Contents: Read and write only.
  • Set an expiration and renew it when it lapses.
  • Store the token in a Web Credential, never in code.
  • If a token ever appears in a screenshot or chat, revoke it in GitHub immediately and create a new one.

Comments

Popular posts from this blog

APEX - Tip: Fix Floating Label Issue

Oracle APEX's Universal Theme provides a modern and clean user experience through features like floating (above) labels for page items.  These floating labels work seamlessly when users manually enter data, automatically moving the label above the field on focus or input.  However, a common UI issue appears when page item values are set Dynamically the label and the value overlap, resulting in a broken and confusing user interface. once the user focuses the affected item even once, the label immediately corrects itself and displays properly. When an issue is reported, several values are populated based on a single user input, causing the UI to appear misaligned and confusing for the end user. Here, I'll share a few tips to fix this issue. For example, employee details are populated based on the Employee name. In this case, the first True Action is used to set the values, and in the second True Action, paste the following code setTimeout(function () {   $("#P29_EMAIL,#P29_...

Oracle APEX UI Tip: Display Page Title Next to the APEX Logo

In most Oracle APEX applications, every page has a Page Title displayed at the top. While useful, this title occupies vertical space, especially in apps where screen real estate matters (dashboards, reports, dense forms). So the goal is simple: Show the page title near the APEX logo instead of consuming page content space. This keeps the UI clean, professional, and consistent across all pages. Instead of placing the page title inside the page body:         ✅ Fetch the current page title dynamically         ✅ Display it right after the APEX logo         ✅ Do it globally, so it works for every page All of this is achieved using:         ✅ Global Page (Page 0)         ✅ One Dynamic Action         ✅ PL/SQL + JavaScript Simple, effective, and reusable. 1️⃣ Create a Global Page Item On Page 0 (Global Page), create a hidden item:      P0_PAGE_TITLE This item wi...

Interactive Grid Tips (Part-1)

Selection Toolbar Events Validation Styling Advanced Selection 1 Get Selected Row Primary Key Return selected row PK values to a page item. Two approaches depending on APEX version. Selection APEX 24.2+ Before 24.x Copy // Initialization JavaScript Function function (options) { options.defaultGridViewOptions = { selectionStateItem: "P22_SELECTED_IDS" }; return options; } Copy var ig$ = apex.region( "EMP" ).call( "getViews" , "grid" ); var model = ig$.model; var selectedIds = []; ig$.view$.grid( "getSelectedRecords" ).forEach( function (rec) { selectedIds.push(model.getValue(rec, "EMPNO" )); }); $s ( "P22_SELECTED_IDS" , ...