Pulp Design Doc
Pulp
Mobile-to-Repo Commit Pipeline
Push content from your phone to any repo in two taps.
The Problem
When working in Claude on mobile, generated files (markdown docs, lead cards, design specs, content drafts) have no direct path to a git repository. The current workflow requires waiting for laptop access, downloading the file, moving it to the correct repo, committing, and pushing. This creates a bottleneck where useful outputs pile up in Claude conversations and never make it into version control.
The problem compounds across multiple repos (learnwithoj-internal, mathbliss, personal projects) that each need files committed to different directories.
The Solution
Pulp is a lightweight serverless API endpoint that accepts content via POST and commits it to the correct GitHub repo using the GitHub API. Paired with an iOS Shortcut, the workflow becomes: copy content from Claude, run the Shortcut, pick a repo, done.
Flow
Claude (phone) → Copy content → iOS Shortcut → Pulp API → GitHub API → Committed file
What Pulp Does
- Accepts a POST request with: content (the file body), filename, target repo, target directory, and an optional commit message
- Looks up the repo in its config to determine which GitHub account and token to use
- Commits the file to the specified path using the GitHub Contents API
- Returns the URL of the committed file
What Pulp Does NOT Do
- No scraping, no browser automation, no OAuth dance on mobile
- No file storage (content passes through to GitHub and is not retained)
- No branch management in v1 (commits to default branch)
Architecture
Platform
Cloudflare Workers (recommended) or Vercel Edge Functions.
Rationale: zero cold starts, generous free tier (Cloudflare Workers: 100k requests/day free), edge deployment means low latency from anywhere, and secrets management built in for storing GitHub tokens.
Authentication
Pulp itself needs to be protected so random people can't commit to your repos. Two layers:
- Bearer token: A secret token you generate and store in both the Worker secrets and the iOS Shortcut. Every request must include
Authorization: Bearer <your-pulp-token>. - GitHub PAT: A single fine-grained personal access token scoped to only the repos Pulp needs, with
contents: writepermission. Stored as a Worker secret.
Config: Repo Registry
The repo registry maps repo identifiers (friendly names you'll see in the iOS Shortcut) to their GitHub coordinates and credentials.
{
"repos": {
"oj-internal": {
"owner": "your-username",
"repo": "learnwithoj-internal",
"default_dir": "docs",
"branch": "main"
},
"oj-leads": {
"owner": "your-username",
"repo": "learnwithoj-internal",
"default_dir": "leads",
"branch": "main"
},
"mathbliss": {
"owner": "your-username",
"repo": "mathbliss",
"default_dir": "content",
"branch": "main"
},
"another-project": {
"owner": "your-username",
"repo": "some-repo",
"default_dir": "",
"branch": "main"
}
}
}
Note: The GitHub PAT is stored as a single Cloudflare Worker secret (GITHUB_PAT), not in the config JSON. You can add a new repo to the registry by adding an entry to this config and redeploying (a one-line wrangler command). All repos are accessed through the same GitHub account.
The oj-leads entry is a good example of how you can have multiple shortcuts into the same repo targeting different directories. When Squeeze is built, its lead cards could use this same endpoint.
The two Claude accounts don't affect Pulp at all — both accounts generate content that you copy to your clipboard, and the iOS Shortcut doesn't care which app the clipboard content came from.
API Specification
Endpoint
POST https://pulp.<your-domain>.workers.dev/commit
Headers
Authorization: Bearer <pulp-token>
Content-Type: application/json
Request Body
{
"repo": "oj-internal",
"filename": "squeeze-design-doc.md",
"content": "# Squeeze\n\nDesign doc content here...",
"dir": "docs/tools",
"message": "Add Squeeze design doc"
}
| Field | Required | Description |
|---|---|---|
repo | Yes | Key from the repo registry (e.g., "oj-internal") |
filename | Yes | Name of the file to create or update |
content | Yes | File content (will be base64 encoded for GitHub API) |
dir | No | Subdirectory within the repo. Falls back to default_dir from config. |
message | No | Commit message. Defaults to Add <filename> via Pulp |
Response (Success)
{
"status": "committed",
"url": "https://github.com/owner/repo/blob/main/docs/tools/squeeze-design-doc.md",
"sha": "abc123...",
"repo": "oj-internal",
"path": "docs/tools/squeeze-design-doc.md"
}
Response (Error)
{
"status": "error",
"message": "Repository 'unknown-repo' not found in registry"
}
Error Codes
| HTTP Status | Meaning |
|---|---|
| 401 | Missing or invalid Pulp bearer token |
| 404 | Repo key not found in registry |
| 409 | File already exists (see Update Behavior below) |
| 422 | Missing required fields |
| 502 | GitHub API error (token expired, permissions, etc.) |
Update Behavior
If a file already exists at the target path, Pulp needs the file's current SHA to update it (GitHub API requirement). Two options:
- v1 (simple): Return a 409 with the existing file's SHA. The iOS Shortcut can retry with the SHA to overwrite.
- v2 (smart): Pulp automatically fetches the current SHA and overwrites. Add an
overwrite: trueflag to the request body.
Recommendation: Start with v2. You almost always want to update existing files, and the extra API call is negligible.
iOS Shortcut Design
The Shortcut is the user-facing interface. It should feel fast and require minimal input.
Quick Commit Flow (most common)
- Copy content from Claude
- Run Shortcut (from share sheet or home screen)
- Pick repo from a list menu (shows friendly names from registry)
- Enter filename (text input with smart default based on first line of content)
- Done — Shortcut POSTs to Pulp, shows success notification with GitHub link
Shortcut Actions
1. Get Clipboard
2. Show Menu: [oj-internal, oj-leads, mathbliss, personal-project]
3. Ask for Input: "Filename" (default: generated from clipboard first line)
4. Optional: Ask for Input: "Subdirectory" (default: use repo default)
5. Build JSON payload
6. POST to Pulp endpoint with Bearer token
7. Parse response
8. Show notification: "Committed to {repo} ✓" with link
Advanced: Paste + Commit Shortcut
For when you want to skip the repo selection because you always commit a certain type of content to the same place:
- "Save Lead Card" — hardcoded to
oj-leads, prompts only for filename - "Save Doc" — hardcoded to
oj-internal/docs, prompts only for filename - "Save Math Post" — hardcoded to
mathbliss/content, prompts only for filename
These are just duplicates of the main Shortcut with the repo pre-selected. Takes it from 5 taps to 3.
Cloudflare Worker Implementation
Project Structure
pulp/
src/
index.ts # Request router and auth check
commit.ts # GitHub commit logic
config.ts # Repo registry
wrangler.toml # Cloudflare config
package.json
Core Logic (commit.ts)
The commit flow against the GitHub Contents API:
1. Look up repo config from registry
2. Retrieve the GitHub PAT from Worker secrets
3. Build the file path: {dir}/{filename}
4. Check if file exists (GET /repos/{owner}/{repo}/contents/{path})
- If exists: capture SHA for update
- If not: proceed with create
5. PUT /repos/{owner}/{repo}/contents/{path}
- body: { message, content (base64), sha (if update), branch }
6. Return the committed file URL and SHA
GitHub API Calls
Check if file exists:
GET https://api.github.com/repos/{owner}/{repo}/contents/{path}?ref={branch}
Headers: Authorization: Bearer {github-pat}
Create or update file:
PUT https://api.github.com/repos/{owner}/{repo}/contents/{path}
Headers: Authorization: Bearer {github-pat}
Body: {
"message": "Add squeeze-design-doc.md via Pulp",
"content": "<base64-encoded-content>",
"branch": "main",
"sha": "<existing-sha-if-updating>"
}
Secrets (via wrangler secret)
PULP_TOKEN # Your bearer token for authenticating to Pulp
GITHUB_PAT # Fine-grained PAT for your GitHub account
GitHub PAT Setup
Creating a Fine-Grained Token
- Go to Settings > Developer Settings > Personal Access Tokens > Fine-grained tokens
- Name: "Pulp"
- Expiration: 90 days (set a calendar reminder to rotate)
- Repository access: select only the repos Pulp needs
- Permissions: Contents (Read and write) — this is the only permission needed
- Generate and save to Cloudflare Worker secrets via
wrangler secret put GITHUB_PAT
Why Fine-Grained Over Classic
Fine-grained tokens are scoped to specific repos with specific permissions. If the token leaks, the blast radius is limited to just file contents in the repos you selected, not your entire GitHub account. Classic tokens give broad access and are not recommended.
Security Considerations
- Bearer token on Pulp: Without this, anyone who discovers your Worker URL can commit to your repos. Generate a strong random token (32+ chars).
- HTTPS only: Cloudflare Workers are HTTPS by default. Content in transit is encrypted.
- No content retention: Pulp passes content through to GitHub and does not store, log, or cache file contents. The only logs are Cloudflare's default request logs (URL, status code, timing) which do not include request bodies.
- Token rotation: Fine-grained PATs expire. Set 90-day expiration and rotate. Update the Worker secret with
wrangler secret put GITHUB_PAT. - Rate limits: GitHub API allows 5,000 requests/hour per authenticated user. You will never hit this with manual mobile commits.
Implementation Phases
Phase 1: Core Endpoint
Build the Cloudflare Worker with single-repo support. Hardcode learnwithoj-internal as the only repo. Test with curl from your terminal. Estimated time: one session.
Phase 2: Multi-Repo Registry
Add the repo registry config. Support switching between repos and target directories. Test all configured repos. Estimated time: one session.
Phase 3: iOS Shortcut
Build the main Shortcut with repo picker menu. Build the specialized shortcuts (Save Lead Card, Save Doc, etc.). Test the full flow from Claude on phone to committed file. Estimated time: one session.
Phase 4: Update Support
Add the auto-fetch SHA logic for updating existing files. Add the overwrite: true flag. Test update scenarios. Estimated time: quick addition.
Phase 5: Quality of Life
- Add a
GET /reposendpoint that returns the registry list (so the iOS Shortcut can dynamically build the menu instead of hardcoding repo names) - Add optional Markdown frontmatter injection (auto-add
created_at,source: pulpto files) - Add a
GET /recentendpoint that shows last 10 commits made through Pulp (for quick verification)
Integration with Squeeze
When Squeeze is built, Pulp becomes its commit layer. Instead of Squeeze having its own git logic, it can POST lead cards to Pulp's endpoint. This means the Squeeze CLI on your laptop and the mobile capture flow both use the same commit path, and lead cards end up in the same place regardless of which device you're on.
The oj-leads repo entry in the registry is pre-configured for this. Squeeze just needs to POST to /commit with repo: "oj-leads" and the lead card content.
Naming Note
Working name is Pulp — push the pulp of the content to where it belongs. Fits the citrus brand alongside Squeeze and Fresh Squeeze. Open to renaming.
Open Questions
- Should Pulp support creating directories that do not exist yet, or require the directory to already exist in the repo?
- Is there value in a "dry run" mode that shows what would be committed without actually committing? Useful for debugging the Shortcut.
- Should the iOS Shortcut support pasting images (screenshots, diagrams) or just text? GitHub Contents API supports binary files via base64 but the Shortcut gets more complex.
- What is the commit author strategy? The PAT's associated GitHub user will be the author. Is that fine, or should commits show a different name (e.g., "Pulp Bot") to distinguish mobile commits from laptop commits?
- Should Pulp eventually support branch creation for PRs instead of direct commits to main? Useful if you want review before merge.
- Is there a need for a "bulk commit" mode where you paste multiple files at once (e.g., a design doc plus its associated lead card)?
- Token rotation reminders: build into the tool or handle externally (calendar reminder, etc.)?
Pulp | Learn with O.J. | learnwithoj.com