Build a Code Hosting Platform

Repos, branches, commits, pull requests, and native AI agent access — a complete GitHub alternative built entirely on drops and entities. This is how repos.infiniteocean.io is built.

Running Drops This tutorial uses Drops, a compact language that replaces curl commands. Run them via POST /exec or paste into the live editor.

A version-controlled code hosting platform seems like it needs a specialized database, a custom file system, and years of engineering. But if your storage layer already has append-only immutable writes, entity keys, version history, bulk operations, and full-text search — you have everything you need.

This tutorial walks through building the actual Ocean Repos product, step by step. By the end, you'll have user handles, repositories with file trees, branches, commits, pull requests, and MCP agent integration — all from a handful of commands.

1Architecture

The entire platform maps to InfiniteOcean routes and entities:

ConceptRouteIO Primitive
User handles_handles/{handle}Entity (key = handle name)
Repo configrepos/{handle}/{repo}/_metaEntity (key = config)
Filesrepos/{handle}/{repo}/{branch}Entity (key = file path)
Branchesrepos/{handle}/{repo}/_branchesEntity (key = branch name)
Commitsrepos/{handle}/{repo}/_commitsEntity (key = commit ID)
Pull requestsrepos/{handle}/{repo}/_pullsEntity (key = PR number)
Repo index_repo_index/{handle}/{repo}Entity (discovery)

Every concept in version control maps to an entity. Files are entities where the key is the file path (src/index.js) and the payload is the file content. Branches are routes — the same file path under different branch routes holds different versions. Commits are append-only log entries. This is the entire architecture.

Key insight InfiniteOcean entities already have version history (?history=true), append-only immutable writes, and tombstones for deletion. You don't need to build versioning — it's built into the storage layer.

2Register a Handle

Before creating repos, users claim a human-readable handle. This is a simple write to the _handles route:

drops
write _handles @niklas { owner_key: "YOUR_KEY", display_name: "Niklas", created_at: "2026-02-13T12:00:00Z" }
_handles
A shared route for all user handles. Readable by anyone, write-protected per owner.
key
The handle name itself. Lowercase, alphanumeric + hyphens, 2–39 characters.
owner_key
The Ocean key that owns this handle. Used for write authorization.

Ownership is enforced application-side: before writing, check if the handle already exists. If it does and the owner_key doesn't match the requester, reject the write. First writer wins.

drops
-- Look up a handle
read _handles @niklas

-- Returns:
-- { "payload": { "owner_key": "YRV...", "display_name": "Niklas" } }

3Create a Repository

A repository is a set of related entities under a route prefix. Creating one means writing a config entity, a default branch, and an initial commit. Use bulk to do it atomically:

drops
bulk [
  write repos/niklas/my-project/_meta @config { name: "my-project", description: "A cool project", default_branch: "main", visibility: "public", owner_key: "YOUR_KEY", language: "javascript" },
  write repos/niklas/my-project/_branches @main { created_at: "2026-02-13T12:00:00Z", status: "active" },
  write repos/niklas/my-project/main @README.md "# my-project\n\nA cool project.\n",
  write repos/niklas/my-project/_commits @init { message: "Initial commit", branch: "main", files: [{ path: "README.md", action: "add" }] },
  write _repo_index @niklas/my-project { name: "my-project", description: "A cool project", handle: "niklas", language: "javascript" }
]

One command creates everything: the repo config, a main branch, a README file, an initial commit, and an index entry for discovery. Bulk accepts up to 1,000 writes in a single request.

_meta/config
Repo settings: name, description, owner, visibility, language.
_branches/main
Branch metadata. The key is the branch name.
main/README.md
A file. The route includes the branch, the key is the file path.
_commits/init
A commit record. Lists which files changed and why.
_repo_index
A flat index for listing all repos on the home page.

4Work with Files

Files are entities where the key is the file path and the payload is the content. Reading and writing files is just reading and writing entities:

drops
-- Write a file
write repos/niklas/my-project/main @src/index.js "export function hello() {\n  return \"world\";\n}\n"

-- Read it back
read repos/niklas/my-project/main @src/index.js

-- List all files in a branch
list repos/niklas/my-project/main

The list command returns all entities in a route. Filter the keys to build a file tree, or read individual files with the read command.

File paths as keys Entity keys can contain /, so paths like src/utils/helpers.js work naturally as keys. No encoding needed.

To see the history of a file, use the ?history flag:

drops
-- Get all versions of a file
read repos/niklas/my-project/main @src/index.js ?history

-- Get the file as it was at a specific time
read repos/niklas/my-project/main @src/index.js at "2026-02-13T10:00:00Z"

Version history and point-in-time reads come free from the storage layer. Every write is an immutable append — nothing is overwritten, nothing is lost.

5Make Commits

A commit is a batch of file writes plus a commit record. Use bulk to make them atomic:

drops
bulk [
  write repos/niklas/my-project/main @src/index.js "export function hello() {\n  return \"world\";\n}\n\nexport function goodbye() {\n  return \"farewell\";\n}\n",
  write repos/niklas/my-project/main @src/utils.js "export const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1);\n",
  write repos/niklas/my-project/_commits @commit-002 { message: "Add goodbye function and utils", branch: "main", author: "niklas", files: [{ path: "src/index.js", action: "modify" }, { path: "src/utils.js", action: "add" }], parent: "init" }
]

The file changes and the commit record are written together. The commit entity records which files changed, the commit message, the author, and a reference to the parent commit. To list commits:

drops
-- List all commits
list repos/niklas/my-project/_commits
Delete a file To delete a file, use the delete command. This creates a tombstone — the read command returns nothing, but the history preserves the deletion event.

6Create Branches

In Git, branches are lightweight pointers into a DAG. In Ocean Repos, branches are separate routes. Creating a branch means copying files from the source branch to a new route:

javascript
// 1. Export all files from source branch
const res = await fetch(
  'https://api.infiniteocean.io/export/repos/niklas/my-project/main?latest=true',
  { headers: { 'X-Ocean-Key': KEY } }
);
const lines = (await res.text()).trim().split('\n');
const entities = lines.map(l => JSON.parse(l)).filter(e => !e.tombstone);

// 2. Build bulk write: copy files to new branch + create branch entity
const drops = entities.map(e => ({
  route: 'repos/niklas/my-project/feature-auth',
  key: e.key,
  payload: typeof e.payload === 'string' ? e.payload : JSON.stringify(e.payload),
}));

drops.push({
  route: 'repos/niklas/my-project/_branches',
  key: 'feature-auth',
  payload: JSON.stringify({
    created_at: new Date().toISOString(),
    source_branch: 'main',
    status: 'active',
  }),
});

// 3. Atomic bulk write
await fetch('https://api.infiniteocean.io/drops', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-Ocean-Key': KEY },
  body: JSON.stringify({ drops }),
});

This approach is simpler than Git's DAG model. Each branch is a full snapshot — no merge base computation, no reachability graphs. The tradeoff is storage (full copies vs pointers), but with append-only storage and S3 backend, the cost is negligible.

main/src/index.js
File in the main branch.
feature-auth/src/index.js
Same file path, different branch route. Independent versions.

7Pull Requests

A pull request is an entity that tracks a proposed merge between branches:

drops
-- Create a pull request
write repos/niklas/my-project/_pulls @1 { title: "Add authentication module", body: "This PR adds login and session management.", source_branch: "feature-auth", target_branch: "main", author: "niklas", state: "open" }

To compute a diff, export both branches and compare:

javascript
// Fetch both branches
const [sourceFiles, targetFiles] = await Promise.all([
  exportEntities('repos/niklas/my-project/feature-auth'),
  exportEntities('repos/niklas/my-project/main'),
]);

// Build maps
const sourceMap = Object.fromEntries(sourceFiles.map(f => [f.key, f.payload]));
const targetMap = Object.fromEntries(targetFiles.map(f => [f.key, f.payload]));

// Find changes
const added = Object.keys(sourceMap).filter(k => !(k in targetMap));
const modified = Object.keys(sourceMap).filter(k =>
  k in targetMap && sourceMap[k] !== targetMap[k]
);
const deleted = Object.keys(targetMap).filter(k => !(k in sourceMap));

Merging is the reverse: copy changed files from the source branch to the target branch, then update the PR state to merged.

Conflict detection Compare modification timestamps: if the same file was modified in both branches after the branch creation time, flag it as a conflict. Entity history timestamps make this straightforward.

8Search Code

InfiniteOcean has built-in hybrid search (BM25 + vector). Search across all files in a branch with a single command:

drops
search repos/niklas/my-project/main "function hello" top 10

Results include the matching entity key (the file path) and a relevance score. The search engine indexes entities automatically — write a file, and it's searchable immediately. No configuration, no external search service.

9Agent Access with MCP

This is where it gets interesting. InfiniteOcean has native MCP (Model Context Protocol) support. Every repo can automatically become an MCP server that AI agents connect to directly. An agent can browse, read, write, and search your code through standard tool calls.

To make a repo available as an MCP server, write a config entity:

drops
write _mcp/servers @repos-niklas-my-project {
  name: "niklas/my-project",
  version: "1.0.0",
  app_key: "YOUR_KEY",
  tools: {
    list_files: {
      description: "List all files in the repository",
      inputSchema: { type: "object", properties: { branch: { type: "string", default: "main" } } },
      function: "functions/_repos"
    },
    read_file: {
      description: "Read a file contents",
      inputSchema: { type: "object", properties: { path: { type: "string" }, branch: { type: "string", default: "main" } }, required: ["path"] },
      function: "functions/_repos"
    },
    write_file: {
      description: "Create or update a file",
      inputSchema: { type: "object", properties: { path: { type: "string" }, content: { type: "string" }, message: { type: "string" }, branch: { type: "string", default: "main" } }, required: ["path", "content", "message"] },
      function: "functions/_repos"
    },
    search_code: {
      description: "Search code in the repository",
      inputSchema: { type: "object", properties: { query: { type: "string" }, branch: { type: "string", default: "main" } }, required: ["query"] },
      function: "functions/_repos"
    }
  }
}

Now agents can connect at https://api.infiniteocean.io/mcp/niklas/my-project. The MCP endpoint speaks JSON-RPC 2.0 and supports initialize, tools/list, and tools/call.

MCP is protocol-specific MCP endpoints use JSON-RPC 2.0 over HTTP. These calls use raw HTTP rather than Drops commands.
bash
# Test the MCP endpoint
curl -X POST "https://api.infiniteocean.io/mcp/niklas/my-project?key=YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'

# List available tools
curl -X POST "https://api.infiniteocean.io/mcp/niklas/my-project?key=YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

# Call a tool
curl -X POST "https://api.infiniteocean.io/mcp/niklas/my-project?key=YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_files","arguments":{"branch":"main"}}}'

To connect from Claude Desktop or Claude Code, add the server URL to your config:

json
{
  "mcpServers": {
    "my-repo": {
      "url": "https://api.infiniteocean.io/mcp/niklas/my-project?key=YOUR_KEY"
    }
  }
}
Agent-native from day one Because every repo is an MCP server, AI agents can work with your code the same way they work with any other tool. An agent can list files, read code, make changes, and create pull requests — all through standard MCP tool calls. No special integration needed.

10The Tool Runner

When an agent calls a tool like read_file, the MCP handler dispatches it to a Python function stored as an entity. The function receives an ocean SDK client and a request object, and uses the same API to read and write data:

python
def main(ocean, request):
    tool = request.get('tool', '')
    args = request.get('arguments', {})
    namespace = request.get('namespace', '')  # e.g. "niklas/my-project"

    if tool == 'list_files':
        branch = args.get('branch', 'main')
        route = f'repos/{namespace}/{branch}'
        entities = ocean.export_entities(route)
        files = [e.get('key') for e in entities
                 if isinstance(e, dict) and e.get('key') and not e.get('tombstone')]
        files.sort()
        return {'files': files, 'count': len(files), 'branch': branch}

    if tool == 'read_file':
        path = args.get('path', '')
        branch = args.get('branch', 'main')
        result = ocean.read(f'repos/{namespace}/{branch}', path)
        return {
            'path': path,
            'content': result.get('payload', ''),
            'size': len(result.get('payload', '')),
        }

    if tool == 'write_file':
        path = args.get('path', '')
        content = args.get('content', '')
        message = args.get('message', '')
        branch = args.get('branch', 'main')
        # payload = content, key = path
        ocean.write(f'repos/{namespace}/{branch}', content, path)
        return {'ok': True, 'path': path, 'message': message}

    return {'error': f'Unknown tool: {tool}'}

This function is stored as an entity at functions/_repos with key main. The MCP config references it via the "function" field in each tool definition. When a tool is called, InfiniteOcean reads the function code, sends it to the runner fleet for execution, and returns the result as an MCP tool response.

ocean.read(route, key)
Read an entity. Returns the latest version.
ocean.write(route, payload, key)
Write a drop. Payload is the content, key identifies the entity.
ocean.export_entities(route)
Export all entities in a route as a list. Used for listing files.
ocean.search(route, text)
Full-text search across entities in a route.

11The Web UI

The web frontend is a single-page app stored as a _site entity, served at repos.infiniteocean.io via InfiniteOcean's view server. The entire UI — file tree, code viewer, commit history, PR pages — is one HTML file with client-side routing.

javascript
// Client-side router
const path = location.pathname;

if (path === '/' || path === '') {
  renderHome(app);                    // List all repos
} else if (path.match(/^\/[^/]+$/)) {
  renderProfile(app, path.slice(1));  // User profile
} else if (path.match(/^\/[^/]+\/[^/]+$/)) {
  const [handle, repo] = path.slice(1).split('/');
  renderRepo(app, handle, repo);     // Repo home
}

// Each render function fetches from the API:
async function renderRepo(app, handle, repo) {
  const [config, files, readme] = await Promise.all([
    readEntity(`repos/${handle}/${repo}/_meta`, 'config'),
    exportEntities(`repos/${handle}/${repo}/main`),
    readEntity(`repos/${handle}/${repo}/main`, 'README.md'),
  ]);
  // ... render file tree, README, stats
}

The SPA reads everything from the InfiniteOcean API. No backend server, no database queries — the frontend calls the same endpoints you've been using throughout this tutorial. Deploying an update is a single write to update the _site/repos-app entity.

12What We Built

From a handful of commands, we built a complete code hosting platform:

No Git, no specialized version control database, no custom file system. Just drops, entities, and the commands documented throughout this tutorial. The append-only storage layer provides immutability and version history. The entity system provides key-value semantics. The list command provides file enumeration. Search provides discovery. MCP provides agent access. And the view server provides hosting.

Try it live Ocean Repos is live at repos.infiniteocean.io. Create a handle, push a repo, and connect it to Claude — every concept in this tutorial is running in production right now.