Skip to content

Instantly share code, notes, and snippets.

@hsingh23
Last active January 6, 2026 09:33
Show Gist options
  • Select an option

  • Save hsingh23/aad4521604942f97876dd296bf14b2a2 to your computer and use it in GitHub Desktop.

Select an option

Save hsingh23/aad4521604942f97876dd296bf14b2a2 to your computer and use it in GitHub Desktop.
Github backend

Using GitHub as a Backend

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.

1. Authentication

GitHub uses OAuth 2.0 for authentication. To interact with a user's repositories, you need an access token with the repo scope.

OAuth Configuration

  • 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.

Implementation: Popup Flow

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.

Recommended Implementation Plan: Onboarding Flow

For the best user experience, follow this standard onboarding sequence:

  1. OAuth2 Authentication: Request the necessary scopes (as detailed above) to get a temporary access token.
  2. 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).
  3. 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);
  });
}

2. The GitHub API Client

The core of the backend is a client that wraps the GitHub REST API.

Initialization

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',
    };
  }
}

User and Repository Metadata

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();
}

3. CRUD Operations for Files

GitHub's file operations are handled via the /contents/ and /git/ endpoints.

Read: Fetching Contents

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;
}

Create / Update: Uploading Files

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();
}

Delete: Removing Files

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 }),
  });
}

Move: Rename/Relocate

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);
}

4. Utilities & Helpers

Helper: File to Base64

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);
  });
}

5. Serving Files

Since GitHub is a git repository, you can serve files directly or via CDNs for better performance and correct MIME types.

Raw GitHub Links

Good for simple text files or small assets. https://raw.githubusercontent.com/:owner/:repo/:branch/:path

CDN (jsDelivr)

Preferred for production assets. It handles caching and provides high availability. https://cdn.jsdelivr.net/gh/:owner/:repo@:branch/:path

Serving HTML (Raw Githack)

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


6. Best Practices & API Limitations

Folder Management

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.

Rate Limiting

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.

Cache Busting

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.


7. React Integration

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] });
    }
  });
}

API Summary Table

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment