This guide walks you through setting up GitMarkdown from scratch, including all required third-party services.
- Prerequisites
- 1. Clone and Install
- 2. Create a Firebase Project
- 3. Enable Firebase Authentication
- 4. Create a GitHub App
- 5. Enable Cloud Firestore
- 6. Enable Realtime Database
- 7. Get Firebase Admin Credentials
- 8. Get Firebase Client Config
- 9. Set Up AI Provider Keys
- 10. Configure Environment Variables
- 11. Apply Security Rules
- 12. Create Firestore Indexes
- 13. Run Locally
- 14. Deploy to Netlify
- Troubleshooting
- Node.js 20+ and npm (or yarn/pnpm)
- A GitHub account
- A Google account (for Firebase)
- An Anthropic and/or OpenAI API key (for AI features)
git clone https://github.com/pooriaarab/gitmarkdown.git
cd gitmarkdown
npm installIf you see peer dependency warnings, that's expected. The project uses an
.npmrcwithlegacy-peer-deps=true.
Copy the environment template:
cp .env.example .env.localYou'll fill in the values in the steps below.
- Go to Firebase Console
- Click "Create a project" (or "Add project")
- Enter a project name (e.g.
gitmarkdown) - Optionally enable Google Analytics (not required)
- Click Create project and wait for it to provision
- In Firebase Console, go to Build > Authentication
- Click Get started
- Go to the Sign-in method tab
- Click GitHub and toggle it Enabled
- You'll see fields for Client ID and Client Secret — leave this page open, you'll fill them in after creating the GitHub OAuth app (next step)
- Go to GitHub Developer Settings > GitHub Apps > New
- Fill in:
- GitHub App name:
GitMarkdown(or any unique name) - Homepage URL:
http://localhost:3000(update to your production URL later) - Callback URL:
https://<YOUR_FIREBASE_PROJECT_ID>.firebaseapp.com/__/auth/handler- Replace
<YOUR_FIREBASE_PROJECT_ID>with your actual Firebase project ID (e.g.gitmarkdown-12345) - You can find this in Firebase Console > Project Settings > General
- Replace
- Request user authorization (OAuth) during installation: Check this box
- Webhook: Uncheck "Active" (not needed)
- GitHub App name:
- Under Permissions, set:
- Repository permissions:
- Contents: Read & write
- Pull requests: Read & write
- Metadata: Read-only (auto-selected)
- Commit statuses: Read-only
- Account permissions:
- Email addresses: Read-only
- Repository permissions:
- Under Where can this GitHub App be installed?, select Any account
- Click Create GitHub App
- On the app settings page:
- Copy the Client ID
- Click Generate a new client secret and copy the Client Secret
- Note the app slug from the URL (e.g. if the URL is
github.com/settings/apps/gitmarkdownapp, the slug isgitmarkdownapp)
- Go back to Firebase Console > Authentication > Sign-in method > GitHub:
- Paste the Client ID and Client Secret
- Click Save
- Add to your
.env.local:
GITHUB_CLIENT_ID=your_client_id_here
GITHUB_CLIENT_SECRET=your_client_secret_here
NEXT_PUBLIC_GITHUB_APP_SLUG=your_app_slug_hereWhile in Firebase Authentication:
- Go to the Settings tab > Authorized domains
- Make sure
localhostis listed (it should be by default) - Later, add your production domain (e.g.
gitmarkdown-app.netlify.app)
- In Firebase Console, go to Build > Firestore Database
- Click Create database
- Choose Start in test mode (we'll apply proper rules later)
- Select a Cloud Firestore location closest to your users (e.g.
us-east1,europe-west1) - Click Enable
This is separate from Firestore — it's used for real-time collaboration (Yjs).
- In Firebase Console, go to Build > Realtime Database
- Click Create Database
- Select a location (use the same region as Firestore if possible)
- Choose Start in locked mode (we'll apply rules later)
- Click Enable
- Copy the database URL shown at the top of the page — it looks like:
https://your-project-default-rtdb.firebaseio.com - Add to your
.env.local:
NEXT_PUBLIC_FIREBASE_DATABASE_URL=https://your-project-default-rtdb.firebaseio.comThe Admin SDK is used server-side (API routes) to verify auth tokens and manage data.
- In Firebase Console, go to Project Settings (gear icon) > Service Accounts
- Make sure Firebase Admin SDK is selected
- Click "Generate new private key"
- A JSON file will be downloaded. Open it and copy these values:
FIREBASE_ADMIN_PROJECT_ID=<"project_id" from JSON>
FIREBASE_ADMIN_CLIENT_EMAIL=<"client_email" from JSON>
FIREBASE_ADMIN_PRIVATE_KEY=<"private_key" from JSON>Important: The private key is a long string that starts with
-----BEGIN RSA PRIVATE KEY-----. Include the entire value including the begin/end lines. In your.env.local, wrap it in double quotes:FIREBASE_ADMIN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----\n"
- In Firebase Console, go to Project Settings (gear icon) > General
- Scroll down to Your apps
- If you don't see a web app, click Add app > Web (
</>)- Enter a nickname (e.g.
gitmarkdown-web) - You don't need Firebase Hosting
- Click Register app
- Enter a nickname (e.g.
- You'll see a
firebaseConfigobject. Copy the values:
NEXT_PUBLIC_FIREBASE_API_KEY=AIza...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-project.firebasestorage.app
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=123456789
NEXT_PUBLIC_FIREBASE_APP_ID=1:123456789:web:abc123At least one AI provider is required for AI features (chat sidebar, inline editing, Mermaid generation). Server-side keys are used by default, but users can also bring their own API keys via the Settings dialog (BYOK).
- Go to Anthropic Console
- Create an API key
- Add to
.env.local:ANTHROPIC_API_KEY=sk-ant-...
- Go to OpenAI Platform
- Create an API key
- Add to
.env.local:OPENAI_API_KEY=sk-...
Your .env.local should now look like this (with your actual values filled in):
# Firebase Client
NEXT_PUBLIC_FIREBASE_API_KEY=AIza...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-project.firebasestorage.app
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=123456789
NEXT_PUBLIC_FIREBASE_APP_ID=1:123456789:web:abc123
NEXT_PUBLIC_FIREBASE_DATABASE_URL=https://your-project-default-rtdb.firebaseio.com
# Firebase Admin
FIREBASE_ADMIN_PROJECT_ID=your-project
FIREBASE_ADMIN_CLIENT_EMAIL=firebase-adminsdk-xxxxx@your-project.iam.gserviceaccount.com
FIREBASE_ADMIN_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----\n"
# GitHub App
GITHUB_CLIENT_ID=Iv1.abc123
GITHUB_CLIENT_SECRET=abc123...
NEXT_PUBLIC_GITHUB_APP_SLUG=gitmarkdownapp
# AI Keys
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_DEFAULT_AI_PROVIDER=anthropic
NEXT_PUBLIC_DEFAULT_AI_MODEL=claude-sonnet-4-20250514
# Security (recommended)
GITHUB_TOKEN_ENCRYPTION_KEY=<output of: openssl rand -hex 32>Go to Firebase Console > Firestore Database > Rules and replace the contents with the rules from firestore.rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper: check if the caller is authenticated
function isAuth() {
return request.auth != null;
}
// Helper: check if caller uid matches the document path userId
function isOwner(userId) {
return isAuth() && request.auth.uid == userId;
}
// ── User-scoped data (owner only) ──────────────────────────
match /users/{userId} {
allow read, write: if isOwner(userId);
match /aiChats/{chatId} {
allow read, write: if isOwner(userId);
}
match /personas/{personaId} {
allow read, write: if isOwner(userId);
}
}
match /userSettings/{userId} {
allow read, write: if isOwner(userId);
}
// ── Webhooks (owner only) ──────────────────────────────────
match /webhooks/{userId}/registrations/{wid} {
allow read, write: if isOwner(userId);
}
// ── Comments (authenticated) ───────────────────────────────
match /comments/{commentId} {
allow read, write: if isAuth();
}
// ── Workspaces (members only) ──────────────────────────────
match /workspaces/{workspaceId} {
function isMember() {
return isAuth() && request.auth.uid in resource.data.members;
}
function isMemberForCreate() {
return isAuth() && request.auth.uid in request.resource.data.members;
}
allow read, update, delete: if isMember();
allow create: if isMemberForCreate();
match /files/{fileId} {
allow read, write: if isAuth()
&& request.auth.uid in get(/databases/$(database)/documents/workspaces/$(workspaceId)).data.members;
match /comments/{commentId} {
allow read, write: if isAuth()
&& request.auth.uid in get(/databases/$(database)/documents/workspaces/$(workspaceId)).data.members;
}
}
match /versions/{versionId} {
allow read, write: if isAuth()
&& request.auth.uid in get(/databases/$(database)/documents/workspaces/$(workspaceId)).data.members;
}
}
}
}
Click Publish.
Go to Firebase Console > Realtime Database > Rules and replace with:
{
"rules": {
"yjs": {
"$workspaceId": {
"$fileId": {
".read": "auth != null",
".write": "auth != null"
}
}
},
".read": false,
".write": false
}
}Click Publish.
The comments feature queries by repoFullName + filePath + createdAt, which requires a composite index.
- Run the app and try to open a file — if the index is missing, you'll see an error in the browser console with a direct link
- Click the link — it takes you to Firebase Console with the index pre-filled
- Click Create index and wait a few minutes
- Go to Firebase Console > Firestore Database > Indexes
- Click Create Index
- Configure:
- Collection ID:
comments - Fields:
repoFullName— AscendingfilePath— AscendingcreatedAt— Ascending
- Query scope: Collection
- Collection ID:
- Click Create and wait for the status to show "Enabled" (takes 1-5 minutes)
npm run devOpen http://localhost:3000. You should see the landing page. Click "Sign in with GitHub" to test the auth flow.
- Landing page loads
- GitHub sign-in completes and redirects to dashboard
- Repos appear on dashboard
- Clicking a repo loads the file tree
- Clicking a markdown file opens the editor
- Making edits triggers auto-save (check the "Saving..." / "Auto-saved" indicator)
- AI sidebar opens and responds to messages (if AI key is configured)
- Comments can be added by selecting text and clicking the comment icon
- Push your code to GitHub (make sure
.env.localis in.gitignore) - Go to Netlify and click "Add new site" > "Import an existing project"
- Connect your GitHub repo
- Configure build settings:
- Build command:
npm run build - Publish directory:
.next
- Build command:
- Go to Site settings > Environment variables and add all variables from your
.env.local - Click Deploy site
npm install -g netlify-cli
netlify login
netlify init # Link to your repo
netlify deploy --prodAfter deploying, update these settings:
.envon Netlify: SetNEXT_PUBLIC_APP_URLto your production URL (e.g.https://gitmarkdown-app.netlify.app)- GitHub App: Update the Homepage URL to your production URL
- Firebase Console > Authentication > Settings > Authorized domains: Add your Netlify domain (e.g.
gitmarkdown-app.netlify.app) FIREBASE_ADMIN_PRIVATE_KEYon Netlify: Make sure the private key is pasted with actual newlines. Netlify's UI handles this correctly if you paste the raw key including\ncharacters.
Make sure this file exists in your project root:
[build]
command = "npm run build"
publish = ".next"
[[plugins]]
package = "@netlify/plugin-nextjs"- You haven't applied the Firestore security rules. See Step 11.
- Or the composite index hasn't been created yet. See Step 12.
Same as above — Firestore rules or missing composite index. Check the browser console for a direct link to create the index.
- Verify the callback URL in your GitHub App matches exactly:
https://<PROJECT_ID>.firebaseapp.com/__/auth/handler - Verify the Client ID and Client Secret from your GitHub App are entered in both
.env.localAND in Firebase Console > Auth > GitHub. - Check that
localhost(for dev) or your production domain is in Firebase Auth > Authorized domains.
- You haven't enabled the GitHub sign-in provider in Firebase Console > Authentication > Sign-in method.
- Make sure
NEXT_PUBLIC_FIREBASE_DATABASE_URLis set and points to your Realtime Database URL. - Make sure the Realtime Database rules allow authenticated access. See Step 11.
- The private key contains newlines. In
.env.local, wrap it in double quotes. - On Netlify, paste the raw key value (the UI handles multiline values correctly).
- If you see
error:0909006C:PEM routines, the key's\ncharacters are being treated as literal text. Make sure they're actual newline characters.
- Check that at least one of
ANTHROPIC_API_KEYorOPENAI_API_KEYis set. - Check the browser Network tab for errors on
/api/ai/chat. - Verify
NEXT_PUBLIC_DEFAULT_AI_PROVIDERmatches a key you've set (anthropicoropenai).
- The project uses
.npmrcwithlegacy-peer-deps=true. If you're using yarn or pnpm, you may need to configure equivalent settings.