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:
- Exports your application using
APEX_EXPORT - Sends the file to GitHub through the GitHub REST API
- 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.comis fine)
Part 1: GitHub Setup
Create the Repository
- Click + (top right) > New repository.
- Name it
apex-backups. - Choose Private, because application code can be sensitive.
- Tick Add a README file. This creates the
mainbranch the API needs. - 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.
- Click your profile picture > Settings.
- Scroll down the left menu and click Developer settings.
- Click Personal access tokens > Fine-grained tokens > Generate new token.
- Set:
- Token name:
apex-backup - Expiration: 30 or 90 days
- Repository access: Only select repositories >
apex-backups
- Token name:
- Under Repository permissions, set Contents to Read and write.
- 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.
- In APEX go to App Builder > Workspace Utilities > Web Credentials > Create.
- 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
- 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
- Create a page with an item
P4_APPLICATION(a select list of application IDs, or a number field). - Add a button
BACKUPwith the action Submit Page. - 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; - Set the Success Message to
Backup completed successfully. - 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
Post a Comment