🛡️ Protect Your GitHub Token: Proxying Private Releases for Electron Apps

When building apps that use GitHub Releases to distribute updates (e.g., via Electron), things are straightforward if your repo is public. But for private GitHub repos, there's a problem:
⚠️ You can’t directly expose the GitHub Release URL to your app without leaking your GitHub token.
In this post, we’ll walk through how to securely proxy GitHub Release downloads through your own server, so that your token stays hidden while your app can still download updates.
🚩 Why You Can’t Use the GitHub URL Directly
GitHub requires authentication for private releases. That means you need to include an Authorization header with a personal access token (PAT):
Authorization: token ghp_123abc...
Including this directly in your frontend or Electron app would be a huge security risk, since users could extract the token and access your private repo.
✅ The Secure Approach: Use a Proxy Server
Instead of downloading from GitHub directly, you set up a backend server that:
Authenticates with GitHub using your token
Downloads the release asset
Proxies the file to the requesting client (your app)
This way, only your server knows the token — the app just talks to your proxy.
🛠 Step-by-Step: Proxy Setup
1. Create a GitHub Token
Create a classic token with:
reposcope (for private repositories)read:packages(if you're using GitHub Packages too)
Save this token in your backend as an environment variable, e.g., GH_TOKEN.
2. Create the Proxy Endpoint
Here’s a full example using Node.js (Express or similar):
// downloadProxy.js
const fetch = require("node-fetch");
const GITHUB_OWNER = "your-org-or-username";
const GITHUB_REPO = "your-repo";
const GH_TOKEN = process.env.GH_TOKEN;
const headers = {
Authorization: `token ${GH_TOKEN}`,
"User-Agent": "SecureReleaseProxy",
Accept: "application/vnd.github.v3+json"
};
async function getLatestRelease() {
const res = await fetch(`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest`, { headers });
if (!res.ok) throw new Error("Failed to fetch latest release");
return await res.json();
}
async function getAssetDownloadUrl(assetName) {
const release = await getLatestRelease();
const asset = release.assets.find(a => a.name === assetName);
if (!asset) throw new Error(`Asset ${assetName} not found`);
return asset.url; // This is the API URL, not a direct download link
}
async function proxyAssetDownload(req, res) {
try {
const { filename } = req.params;
const assetApiUrl = await getAssetDownloadUrl(filename);
const githubRes = await fetch(assetApiUrl, {
headers: {
...headers,
Accept: "application/octet-stream"
}
});
if (!githubRes.ok) {
res.status(500).send("Error fetching asset");
return;
}
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
res.setHeader("Content-Type", "application/octet-stream");
githubRes.body.pipe(res);
} catch (err) {
console.error("Proxy error:", err);
res.status(500).send("Proxy error");
}
}
module.exports = { proxyAssetDownload };
3. Add the Express Route
In your main server file:
const express = require("express");
const { proxyAssetDownload } = require("./downloadProxy");
const app = express();
const PORT = 3200;
app.get("/updates/download/:filename", proxyAssetDownload);
app.listen(PORT, () => {
console.log(`Proxy server listening on http://localhost:${PORT}`);
});
4. Configure Electron Auto-Updater
In your Electron app (using electron-updater), point to your proxy instead of GitHub:
{
"publish": [
{
"provider": "generic",
"url": "http://localhost:3200/updates"
}
]
}
Your latest.yml file should also have URLs that match your proxy endpoint:
version: 1.0.0
files:
- url: Lets-Flo-Desktop-1.0.0-mac.zip
sha512: abc123...
size: 12345678
path: Lets-Flo-Desktop-1.0.0-mac.zip
sha512: abc123...
releaseDate: '2025-05-24T08:00:00.000Z'
🔐 Security Best Practices
Store the
GH_TOKENonly on your backendSet strict scopes (no
write,delete, oradmin)Use environment variables, not hardcoded strings
Add rate limiting / IP restrictions if possible
💡 Bonus: Cache the Release Info
To reduce GitHub API calls, you can cache the latest release metadata in memory or Redis and refresh it every X minutes.
✅ Summary
| Step | What You Do |
| Create GH token | With repo access only |
| Setup proxy server | Download and stream release via backend |
Use Electron generic | Point to your server instead of GitHub |
| Hide token | Never expose token to frontend/app |
By proxying downloads through a secure server, you protect your GitHub credentials while still delivering updates seamlessly to your users.




