GitHub can serve as a powerful, free versioned file storage and robust backend for applications that primarily need file storage, versioning, and user authentication. This guide explores how to implement a GitHub-based backend using the REST API, based on the implementation in this project.
GitHub uses OAuth 2.0 for authentication. To interact with a user's repositories, you need an access token with the repo scope.
- Client ID:
7e08e016048000bc594e(This is the fixed Client ID for the production app). - Scopes:
repo: (Primary) Full access to private and public repositories. Required for file CRUD and repository management.workflow: Required if the backend needs to create or update GitHub Actions workflow files.read:user: Recommended for retrieving basic profile metadata (avatar, name).user:email: Requested if the application needs the user's primary email address for identification.
- Redirect URI: Where GitHub sends the user after authorization.
Using a popup for authentication provides a seamless user experience. A proxy server (like auth.mavo.io) is often used to handle the token exchange securely.
Tip
New to GitHub? If your users don't have an account yet, recommend they watch this tutorial on how to sign up.
For the best user experience, follow this standard onboarding sequence:
- OAuth2 Authentication: Request the necessary scopes (as detailed above) to get a temporary access token.
- Repository Selection/Creation:
- List the user's existing repositories (
getUserRepos). - Allow the user to select an existing repo or create a new one specifically for your app (
createRepo).
- List the user's existing repositories (
- Fully Logged In: Once a repository is linked, the user is ready to perform CRUD operations.
const GITHUB_AUTHORIZE_URL = 'https://github.com/login/oauth/authorize';
const GITHUB_CLIENT_ID = '7e08e016048000bc594e';
const AUTH_SERVER_URL = 'https://auth.mavo.io'; // Proxy for token exchange
export function loginWithGitHub(): Promise<{token: string, backend: string}> {
return new Promise((resolve, reject) => {
const authUrl = new URL(GITHUB_AUTHORIZE_URL);
authUrl.searchParams.set('client_id', GITHUB_CLIENT_ID);
authUrl.searchParams.set('redirect_uri', AUTH_SERVER_URL);
authUrl.searchParams.set('scope', 'repo workflow read:user');
authUrl.searchParams.set('state', JSON.stringify({ url: window.location.origin, backend: 'Github' }));
const popup = window.open(authUrl.toString(), 'authWindow', 'width=800,height=600,popup=1');
const handleMessage = (event: MessageEvent) => {
if (event.origin !== AUTH_SERVER_URL) return;
const { token } = event.data || {};
if (token) {
window.removeEventListener('message', handleMessage);
resolve({ token, backend: 'Github' });
}
};
window.addEventListener('message', handleMessage);
});
}The core of the backend is a client that wraps the GitHub REST API.
All requests must include the Authorization header and the correct Accept header.
const API_BASE = 'https://api.github.com';
class GitHubClient {
private token: string;
constructor(token: string) {
this.token = token;
}
private headers() {
return {
Authorization: `token ${this.token}`,
Accept: 'application/vnd.github.v3+json',
};
}
}async getUser() {
const res = await fetch(`${API_BASE}/user`, { headers: this.headers() });
return res.json();
}
async getUserRepos() {
const res = await fetch(`${API_BASE}/user/repos?sort=updated`, { headers: this.headers() });
return res.json();
}GitHub's file operations are handled via the /contents/ and /git/ endpoints.
You can fetch files in a specific path or get the entire directory tree.
// Get immediate children of a path
async getPathContents(owner: string, repo: string, path: string = '') {
const res = await fetch(`${API_BASE}/repos/${owner}/${repo}/contents/${path}`, {
headers: { ...this.headers(), 'If-None-Match': '' } // Prevent caching
});
return res.json();
}
// Get recursive tree (useful for file explorers)
async getRecursiveTree(owner: string, repo: string, branch: string = 'main') {
const res = await fetch(`${API_BASE}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`, {
headers: this.headers()
});
const data = await res.json();
return data.tree;
}Files must be Base64 encoded. To update an existing file, you must provide its SHA.
async uploadFile(owner: string, repo: string, path: string, content: string, message: string, sha?: string) {
const body: any = { message, content }; // content is base64 string
if (sha) body.sha = sha;
const res = await fetch(`${API_BASE}/repos/${owner}/${repo}/contents/${path}`, {
method: 'PUT',
headers: this.headers(),
body: JSON.stringify(body),
});
return res.json();
}Deletions also require the file's SHA.
async deleteFile(owner: string, repo: string, path: string, sha: string) {
await fetch(`${API_BASE}/repos/${owner}/${repo}/contents/${path}`, {
method: 'DELETE',
headers: this.headers(),
body: JSON.stringify({ message: `Delete ${path}`, sha }),
});
}GitHub doesn't have a direct "move" API. You must copy the file to the new path and then delete the original.
async moveFile(owner: string, repo: string, oldPath: string, newPath: string, sha: string) {
// 1. Fetch original content
const res = await fetch(`${API_BASE}/repos/${owner}/${repo}/git/blobs/${sha}`, { headers: this.headers() });
const blob = await res.json();
// 2. Upload to new path
await this.uploadFile(owner, repo, newPath, blob.content.replace(/\n/g, ''), `Move to ${newPath}`);
// 3. Delete old file
await this.deleteFile(owner, repo, oldPath, sha);
}Browsers' FileReader API can be used to convert files to the Base64 format required by GitHub.
export function fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const result = reader.result as string;
// Remove data URL prefix (e.g., "data:image/png;base64,")
const base64 = result.split(',')[1];
resolve(base64);
};
reader.onerror = (error) => reject(error);
});
}Since GitHub is a git repository, you can serve files directly or via CDNs for better performance and correct MIME types.
Good for simple text files or small assets.
https://raw.githubusercontent.com/:owner/:repo/:branch/:path
Preferred for production assets. It handles caching and provides high availability.
https://cdn.jsdelivr.net/gh/:owner/:repo@:branch/:path
If you want to render HTML files (e.g., for a static site generator), raw GitHub links will serve them as text/plain. Use Raw Githack to serve them with the correct headers:
https://raw.githack.com/:owner/:repo/:branch/:path
Git does not track empty folders. To "create" a folder, upload a dummy file like .keep inside it. To "delete" a folder, you must recursively delete all files within it.
Authenticated requests are limited to 5,000 requests per hour. For batch operations (like moving a folder), implement a small delay (setTimeout) between requests to avoid triggering abuse detection.
GitHub's API can be aggressively cached. Use headers like If-None-Match: "" or append a timestamp query parameter ?t=... to ensure you get the latest data.
Use hooks like useQuery and useMutation from @tanstack/react-query to manage the state and provide optimistic updates for a snappy UI.
export function useFileOperations() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ token, repo, path, file }) => {
const client = new GitHubClient(token);
return client.uploadFile(repo.owner.login, repo.name, path, file, 'Upload');
},
onSuccess: (_, vars) => {
// Invalidate queries to refresh the file list
queryClient.invalidateQueries({ queryKey: ['contents', vars.repo.id] });
}
});
}| Operation | Method | Endpoint | Note |
|---|---|---|---|
| Get User | GET |
/user |
Requires auth |
| List Repos | GET |
/user/repos |
Sortable by updated |
| List Files | GET |
/repos/:owner/:repo/contents/:path |
Returns array |
| Get Tree | GET |
/repos/:owner/:repo/git/trees/:branch |
Use ?recursive=1 |
| Upload File | PUT |
/repos/:owner/:repo/contents/:path |
Content must be Base64 |
| Delete File | DELETE |
/repos/:owner/:repo/contents/:path |
Requires sha |
| Get History | GET |
/repos/:owner/:repo/commits?path=:path |
File-specific commits |
| Get Blob | GET |
/repos/:owner/:repo/git/blobs/:sha |
Raw content via SHA |