Documentation
Minimal tracking infrastructure for developers. Free forever, privacy-first, and self-hostable. Track your coding time with real-time dashboards, global leaderboards, and an extensive plugin system.
Overview
CodeTrackr is a free forever coding time tracker built with Rust. It provides API endpoints, real-time dashboards via WebSocket, global leaderboards, and a powerful plugin system for extensibility.
Key Features
- Free Forever: Core tracking features at no cost — no credit card, no trial limits
- Drop-in API: Compatible with extensions for VS Code and more
- Real-time Dashboard: Live stats via WebSocket
- Global Leaderboards: Compete with developers worldwide
- Plugin System: Browser widgets and server-side lifecycle hooks
- Theme Store: Community CSS themes with live customization
- Self-hostable: Your data, your server, your rules
- Privacy First: Anonymous accounts available, no tracking
Technology Stack
- Backend: Rust with Axum framework
- Database: PostgreSQL with SQLx
- Cache: Redis for real-time data
- Frontend: Vanilla JavaScript with CSS custom properties
- Auth: OAuth (GitHub/GitLab) + Anonymous accounts
- Plugins: QuickJS sandbox for server-side execution
Authentication
CodeTrackr supports multiple authentication methods:
OAuth Providers
- GitHub: Full OAuth integration with profile sync
- GitLab: OAuth support for GitLab users
Anonymous Accounts
Create anonymous accounts without any OAuth provider:
# Create anonymous account
curl -X POST https://codetrackr.leapcell.app/auth/anonymous/create \
-H "Content-Type: application/json" \
-d '{}'
# Response: { "account_number": "A123456", "token": "jwt_token" }
# Login with account number
curl -X POST https://codetrackr.leapcell.app/auth/anonymous/login \
-H "Content-Type: application/json" \
-d '{ "account_number": "A123456" }'
API Keys
Generate API keys for programmatic access:
# List API keys
curl -X GET https://codetrackr.leapcell.app/api/v1/keys \
-H "Authorization: Bearer YOUR_TOKEN"
# Create new API key
curl -X POST https://codetrackr.leapcell.app/api/v1/keys \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "My IDE Extension" }'
Real-time Updates
CodeTrackr provides real-time dashboard updates via WebSocket:
WebSocket Connection
// Get a single-use WebSocket ticket
const response = await fetch('/api/v1/ws-ticket', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token }
});
const { ticket } = await response.json();
// Connect with the ticket (expires in 30s)
const ws = new WebSocket(`wss://codetrackr.leapcell.app/ws?ticket=${ticket}`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Real-time update:', data);
// Update dashboard stats live
};
Real-time Events
- New Heartbeat: Updates coding time instantly
- Stats Changes: Refreshes dashboard statistics
- Leaderboard Updates: Live position changes
- Plugin Events: Real-time plugin data updates
Rate Limiting
CodeTrackr implements intelligent rate limiting with real IP detection:
IP Detection Priority
- Cloudflare Connecting IP header
- True Client IP header
- X-Forwarded-For (first IP)
- X-Real-IP header
- X-Client-IP header
- Connection IP (fallback)
Rate Limits
| Endpoint Type | Limit | Burst |
|---|---|---|
| General API | 200 req/s | 50 burst |
| Authentication | 100 req/s | 20 burst |
| Webhooks | 10 req/s | 5 burst |
Plugin Editor
The Plugin Editor (/cteditor) is a browser-based IDE where you can write, test, and preview your plugins before publishing them to the store. It supports both widget plugins (rendered in the browser) and lifecycle plugins (executed server-side in QuickJS).
Opening the editor
Log in and click Plugin Editor in your dashboard navigation. The editor opens with a template already loaded — click Template at any time to reload it.
Interface overview
| Element | Description |
|---|---|
| Editor pane (left) | Monaco-style JavaScript editor with syntax highlighting, bracket matching, and keyboard shortcuts. |
| Output pane (right) | Shows console output, errors, and exit status for lifecycle plugins — or the rendered DOM for widget plugins. |
Template button | Loads the default plugin template and clears the output. |
Clear button | Empties the editor and the output pane. |
▶ Run button | Executes the current code using the selected simulate mode. Shortcut: Cmd+Enter / Ctrl+Enter. |
Simulate modes
Select the trigger to simulate using the buttons in the bottom bar of the editor pane:
| Mode | What it does |
|---|---|
on_heartbeat | Calls your on_heartbeat(ctx, heartbeat) function with a mock heartbeat object. Runs in the QuickJS sandbox on the server. |
on_tick | Calls your on_tick(ctx) function. Use for scheduled background tasks. |
on_install | Calls your on_install(ctx) function. Simulates a fresh install. |
widget | Executes your script in the browser with a live container DOM element and your real token. The output pane renders the widget HTML directly. |
Widget mode — live preview
Switch to widget mode when writing dashboard panel scripts. Your code receives a real container div and your authenticated token, so API calls work exactly as they will in production. The output pane renders the widget live — you can interact with buttons, file inputs, and any DOM elements your script creates.
// Example: renders a live counter widget in the editor output
container.innerHTML = '<div style="font-size:28px; font-family:var(--font-mono);">Loading…</div>';
fetch('/api/v1/stats/summary?range=7d', {
headers: { 'Authorization': 'Bearer ' + token }
})
.then(r => r.json())
.then(data => {
const s = data.total_seconds || 0;
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
container.querySelector('div').textContent = h + 'h ' + m + 'm';
});
Lifecycle mode — QuickJS sandbox
When using on_heartbeat, on_tick, or on_install modes, the code is sent to the server and executed inside an isolated QuickJS runtime with no access to the filesystem, network, or OS. Available globals are ctx, heartbeat, log(), warn(), and error(). Any call to log() appears in the Output pane.
async function on_heartbeat(ctx, heartbeat) {
log('Project:', heartbeat.project);
log('Language:', heartbeat.language);
// Real DB query — runs server-side
const rows = await ctx.db.query(
'SELECT COUNT(*) as n FROM heartbeats WHERE user_id = $1',
[ctx.user_id]
);
log('Total heartbeats:', rows[0].n);
}
Keyboard shortcuts
| Shortcut | Action |
|---|---|
Cmd/Ctrl + Enter | Run code |
Cmd/Ctrl + / | Toggle comment |
Widget Plugins
Widget plugins add custom panels to a user's dashboard. They are plain JavaScript scripts executed
directly in the user's browser. You have full DOM access, fetch, and any standard
browser API. No server recompilation, no external hosting required.
How it works
When a user visits their dashboard, CodeTrackr fetches all their installed plugins and runs each plugin's script in the browser. Your script receives two arguments:
container— adivDOM element where your plugin should render its content.token— the user's Bearer token, so you can call the CodeTrackr API authenticated.
Your script must populate container with whatever HTML or text you want to display. The
script body is executed directly — you do not need to define a function named
render; just write the logic at the top level.
Step 1 — Write your script
The simplest possible plugin displays a static counter:
container.textContent = 'Hello from my plugin!';
A real plugin calls the CodeTrackr API using the provided token. Here is an example that shows the user's total coding time this week:
container.textContent = 'Loading…';
fetch('/api/v1/stats/summary?range=7d', {
headers: { 'Authorization': 'Bearer ' + token }
})
.then(function(r) { return r.json(); })
.then(function(data) {
var s = data.total_seconds || 0;
var h = Math.floor(s / 3600);
var m = Math.floor((s % 3600) / 60);
container.textContent = h + 'h ' + m + 'm this week';
})
.catch(function() {
container.textContent = '—';
});
Step 2 — Build the panel HTML (optional)
You can set innerHTML to render richer UI using the design tokens from
main.css. The container already lives inside a .plugin-panel card.
fetch('/api/v1/stats/languages?range=7d', {
headers: { 'Authorization': 'Bearer ' + token }
})
.then(function(r) { return r.json(); })
.then(function(data) {
var langs = (data.languages || []).slice(0, 3);
var rows = langs.map(function(l) {
return '<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:6px;">'
+ '<span style="color:var(--text-main);">' + l.language + '</span>'
+ '<span style="color:var(--text-dark);">' + l.percentage.toFixed(1) + '%</span>'
+ '</div>';
}).join('');
container.innerHTML = rows || '<span style="color:var(--text-muted);">No data yet</span>';
});
Step 3 — Available API endpoints
Your script can call any authenticated CodeTrackr endpoint using the token argument.
The most useful ones for plugins are:
| Endpoint | Description |
|---|---|
GET /api/v1/stats/summary?range=7d |
Total time, top language, top project for a period (7d, 30d, all) |
GET /api/v1/stats/languages?range=7d |
Language breakdown with percentages |
GET /api/v1/stats/daily?range=7d |
Per-day seconds array for chart rendering |
GET /api/v1/stats/projects |
Time per project with last heartbeat |
GET /api/v1/stats/streaks |
Current and longest coding streaks |
GET /api/v1/user/me |
Authenticated user profile |
All requests require the header Authorization: Bearer <token>, which is provided
automatically via the token argument.
Step 4 — Use CSS design tokens for theme compatibility
Your plugin runs inside a sandboxed iframe that automatically inherits the active theme's
CSS custom properties. Use these variables instead of hard-coded colors so your plugin looks correct
in every theme — including user-installed ones.
| Variable | Controls |
|---|---|
--bg | Page background |
--bg-card | Card and panel background |
--bg-input | Input field background |
--bg-hover | Hover state background |
--text-main | Primary text color |
--text-muted | Secondary / muted text |
--text-dark | Dimmed label text |
--border | Default border color |
--border-focus | Focused / active border |
--accent | Accent / highlight color |
A JavaScript variable isDark is also available in your script scope — it is
true when the active theme is dark, false when light. Use it for
colors that cannot be expressed as a CSS variable (e.g. canvas fills, SVG strokes).
// Theme-aware colors — works in light and dark themes
container.innerHTML =
'<div style="color:var(--text-main); background:var(--bg-card); border:1px solid var(--border); border-radius:6px; padding:12px;">'
+ '<span style="color:var(--text-muted);">Total this week</span>'
+ '</div>';
// Use isDark for values that can't be CSS variables
var emptyColor = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)';
Step 5 — Handle errors gracefully
Always handle fetch errors and empty states so the panel does not break the dashboard:
fetch('/api/v1/stats/streaks', {
headers: { 'Authorization': 'Bearer ' + token }
})
.then(function(r) {
if (!r.ok) throw new Error('API error');
return r.json();
})
.then(function(data) {
container.textContent = (data.current_streak || 0) + ' day streak 🔥';
})
.catch(function() {
container.innerHTML = '<span style="color:var(--text-muted);">—</span>';
});
Step 5 — Test in the editor first
Open the Plugin Editor, paste your script, select widget mode in the simulate bar, and click ▶ Run. The output pane renders your widget live with your real token — full interactive preview before publishing.
Step 6 — Publish your plugin
Once your script is ready, publish it through the dashboard UI or via the API:
- Log in and click + Publish Plugin in your dashboard.
- Fill in the name (kebab-case, e.g.
streak-counter), display name, description, version, icon emoji, and widget type. - Paste your JavaScript code in the Plugin Script field.
- Click Publish.
Or publish directly via the API:
curl -X POST https://codetrackr.leapcell.app/api/v1/store/publish \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "streak-counter",
"display_name": "Streak Counter",
"description": "Shows your current coding streak.",
"version": "1.0.0",
"icon": "🔥",
"widget_type": "counter",
"script": "fetch(\"/api/v1/stats/streaks\",{headers:{\"Authorization\":\"Bearer \"+token}}).then(r=>r.json()).then(d=>{container.textContent=(d.current_streak||0)+\" day streak\";}).catch(()=>{container.textContent=\"—\";});"
}'
Step 7 — Install and test
After publishing, find your plugin in the Plugin Store section of the home page and
click Install. Go to your dashboard — the panel will appear automatically. If the
script has a runtime error, a warning ⚠ Plugin error will be shown and the full error
will be logged to the browser console.
Security constraints
- Scripts run inside
new Function()— they have access to standard browser APIs but cannot access variables from the CodeTrackr application scope. - Plugins that are reported and banned by admins will stop loading for all users.
- Never hardcode secrets in your script — the
tokenargument is always the authenticated user's own token. - External fetch calls from the script are allowed (runs in the user's browser), but consider that users may have privacy extensions that block cross-origin requests.
Full example — Daily Activity Bars
A complete plugin that renders a 7-day bar chart inside the panel:
var bar_style = 'flex:1;background:var(--border-focus);border-radius:2px 2px 0 0;min-height:2px;';
container.innerHTML = '<div id="ct-bars" style="display:flex;align-items:flex-end;gap:4px;height:60px;"></div>';
fetch('/api/v1/stats/daily?range=7d', {
headers: { 'Authorization': 'Bearer ' + token }
})
.then(function(r) { return r.json(); })
.then(function(data) {
var daily = data.daily || [];
if (daily.length === 0) {
container.textContent = 'No data yet';
return;
}
var max = Math.max.apply(null, daily.map(function(d) { return d.seconds; })) || 1;
var barsEl = document.getElementById('ct-bars');
if (!barsEl) return;
barsEl.innerHTML = daily.map(function(d) {
var pct = Math.max((d.seconds / max) * 100, 2).toFixed(1);
var h = Math.floor(d.seconds / 3600);
var m = Math.floor((d.seconds % 3600) / 60);
return '<div title="' + d.date + ' · ' + h + 'h ' + m + 'm" style="' + bar_style + 'height:' + pct + '%;"></div>';
}).join('');
})
.catch(function() {
container.textContent = '—';
});
Lifecycle Plugins
Lifecycle plugins run server-side inside an embedded QuickJS sandbox. They react to events like incoming heartbeats, periodic ticks, or a fresh install, and can expose custom HTTP endpoints. Unlike widget plugins, they have no DOM or browser access — instead they receive a ctx object with real database and Redis access scoped to the authenticated user.
When to use lifecycle plugins
- Aggregate or transform user data every time a heartbeat arrives.
- Run scheduled background logic on a periodic tick.
- Set up initial state the moment a user installs your plugin.
- Expose custom POST endpoints callable from widget plugins or external tools.
- Let users trigger account-level actions (delete data, export stats, etc.) via RPC.
The ctx object
Every handler receives ctx as its first argument. All properties are scoped to the authenticated user — you can never read or modify data belonging to other users.
| Property | Type | Description |
|---|---|---|
ctx.user_id | string | UUID of the authenticated user who owns this plugin instance. |
ctx.db.query(sql, params) | Promise<Row[]> | Execute a parameterized PostgreSQL query. Returns an array of row objects. SQL must target allowed tables only (see below). |
ctx.redis.get(key) | Promise<string|null> | Get a value from your plugin's private Redis namespace. |
ctx.redis.set(key, value) | Promise<void> | Set a key in your plugin's private Redis namespace. |
ctx.redis.del(key) | Promise<void> | Delete a key from your plugin's private Redis namespace. |
ctx.redis.incr(key) | Promise<number> | Atomically increment an integer counter in your private namespace. |
ctx.config.base_url | string | Base URL of the CodeTrackr instance, e.g. https://codetrackr.leapcell.app. |
Lifecycle hooks
Export any combination of these three functions. You do not need to export all of them — only the ones your plugin uses.
// Called every time a heartbeat is received for this user.
async function on_heartbeat(ctx, heartbeat) {
log('Heartbeat from', heartbeat.editor, 'on', heartbeat.project);
}
// Called on a periodic server-side tick (approximately every minute).
async function on_tick(ctx) {
log('Tick for user', ctx.user_id);
}
// Called exactly once when a user installs your plugin.
async function on_install(ctx) {
log('Plugin installed for', ctx.user_id);
}
The heartbeat object
on_heartbeat receives the raw heartbeat that was just recorded:
| Field | Type | Description |
|---|---|---|
project | string | Project or workspace name |
language | string | Programming language detected |
file | string | Relative file path (may be empty) |
editor | string | Editor name, e.g. VS Code, Neovim |
branch | string | Git branch (may be empty) |
os | string | Operating system |
machine | string | Machine identifier (may be empty) |
duration | number | Duration in seconds |
time | number | Unix timestamp of the activity |
Logging
Use log(), warn(), and error() to emit output. In the Plugin Editor they appear in the Output pane in real time. In production they are written to the server trace log — not visible to end users.
log('Hello', 'world'); // Hello world
warn('Something unusual'); // [WARN] Something unusual
error('Something broke'); // [ERROR] Something broke
// All functions accept multiple arguments — they are joined with spaces:
log('Project:', heartbeat.project, '— duration:', heartbeat.duration + 's');
Database queries
Use ctx.db.query(sql, params) with positional placeholders ($1, $2, …). Parameters are always passed as an array. The query runs synchronously inside the sandbox and returns an array of plain row objects.
Always filter by ctx.user_id — the sandbox does not add a user filter automatically. Queries that omit a user scope will read data from all users.
async function on_heartbeat(ctx, heartbeat) {
// Count total heartbeats for this user on this project
const rows = await ctx.db.query(
`SELECT COUNT(*) as n, SUM(duration_seconds) as total_secs
FROM heartbeats
WHERE user_id = $1 AND project = $2`,
[ctx.user_id, heartbeat.project]
);
log('Sessions on', heartbeat.project + ':', rows[0].n, '— total', rows[0].total_secs + 's');
}
Allowed database tables
For security, plugins can only query the following tables. Attempts to access any other table are rejected with an error before the query reaches the database:
| Table | Description |
|---|---|
heartbeats | Raw coding activity events |
projects | Project metadata |
daily_stats_cache | Pre-computed daily totals |
plugin_store | Plugin registry entries |
installed_plugins | Which plugins a user has installed |
plugin_settings | Per-user plugin configuration (JSONB) |
plugin_reviews | User-written reviews for plugins |
users | User profile and preferences (read/update own record only) |
api_keys | User API keys (manage own keys only) |
user_follows | Follow relationships between users |
Allowed SQL commands are SELECT, INSERT, UPDATE, and DELETE. DDL commands (DROP, CREATE, ALTER, TRUNCATE, etc.) are always rejected.
Redis — private namespace
All Redis keys are automatically namespaced to your plugin and user. The key you pass to ctx.redis.* is prefixed with plugin:<plugin-name>:<user-id>: before any operation — you cannot read or overwrite keys belonging to another plugin or another user.
async function on_heartbeat(ctx, heartbeat) {
// Stored as: plugin:my-plugin:<user-id>:count:<project>
const count = await ctx.redis.incr('count:' + heartbeat.project);
log('Heartbeat #' + count + ' on', heartbeat.project);
// Cache last seen language
await ctx.redis.set('last-lang', heartbeat.language);
}
async function on_tick(ctx) {
const lang = await ctx.redis.get('last-lang');
log('Last language:', lang || 'none');
}
async function on_install(ctx) {
// Run once — initialize default state
await ctx.redis.set('installed-at', new Date().toISOString());
}
Plugin settings (per-user config)
Store user-configurable settings in the plugin_settings table. The settings column is a JSONB object — query and update it like any other table.
async function on_install(ctx) {
// Create default settings for this user on install
await ctx.db.query(
`INSERT INTO plugin_settings (user_id, plugin, settings)
VALUES ($1, $2, $3)
ON CONFLICT (user_id, plugin) DO NOTHING`,
[ctx.user_id, 'my-plugin', JSON.stringify({ notify: true, threshold: 3600 })]
);
}
async function on_tick(ctx) {
const rows = await ctx.db.query(
'SELECT settings FROM plugin_settings WHERE user_id = $1 AND plugin = $2',
[ctx.user_id, 'my-plugin']
);
const settings = rows[0]?.settings || {};
if (settings.notify) {
log('Notifications enabled — threshold:', settings.threshold, 's');
}
}
Custom HTTP endpoints (RPC)
Declare an endpoints object to expose custom POST routes. Each key becomes a handler available at:
POST /api/v1/plugins/<plugin-name>/rpc/<handler-name>
Every handler receives (ctx, req) where req is the parsed JSON body of the request. Return any JSON-serializable value — it becomes the response body.
const endpoints = {
// POST /api/v1/plugins/my-plugin/rpc/stats
'stats': async (ctx, req) => {
const rows = await ctx.db.query(
`SELECT COUNT(*) as sessions, SUM(duration_seconds) as total_secs
FROM heartbeats WHERE user_id = $1`,
[ctx.user_id]
);
return {
sessions: rows[0].sessions,
total_hours: (rows[0].total_secs / 3600).toFixed(1)
};
},
// POST /api/v1/plugins/my-plugin/rpc/set-threshold
// Body: { "threshold": 7200 }
'set-threshold': async (ctx, req) => {
const threshold = Number(req.threshold) || 3600;
await ctx.db.query(
`UPDATE plugin_settings SET settings = settings || $1
WHERE user_id = $2 AND plugin = $3`,
[JSON.stringify({ threshold }), ctx.user_id, 'my-plugin']
);
return { ok: true, threshold };
},
// POST /api/v1/plugins/my-plugin/rpc/delete-account
'delete-account': async (ctx, req) => {
await ctx.db.query(
'DELETE FROM users WHERE id = $1',
[ctx.user_id]
);
return { ok: true };
},
};
Call your endpoint with Bearer auth:
# No body required
curl -X POST https://codetrackr.leapcell.app/api/v1/plugins/my-plugin/rpc/stats \
-H "Authorization: Bearer YOUR_TOKEN"
# With a JSON body
curl -X POST https://codetrackr.leapcell.app/api/v1/plugins/my-plugin/rpc/set-threshold \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "threshold": 7200 }'
Handler names may only contain alphanumeric characters, hyphens, and underscores. Any other characters are stripped before dispatch.
Calling RPC from a widget plugin
Lifecycle RPC endpoints are ideal as the data backend for your widget plugin. Call them from the widget script using the provided token:
// In your widget script (runs in the browser)
fetch('/api/v1/plugins/my-plugin/rpc/stats', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token }
})
.then(r => r.json())
.then(data => {
container.textContent = data.total_hours + 'h total';
})
.catch(() => {
container.textContent = '—';
});
Sandbox limits
| Limit | Value |
|---|---|
| Memory | 16 MB per execution |
| Stack size | 512 KB |
| Execution timeout | 15 seconds (both lifecycle hooks and RPC handlers) |
| Network access | None — fetch, XMLHttpRequest, and sockets are not available |
| Filesystem access | None — require, fs, and Node.js APIs are not available |
| Browser globals | None — window, document, localStorage, etc. are not available |
| Allowed SQL commands | SELECT, INSERT, UPDATE, DELETE |
Error handling
Unhandled exceptions inside a handler are caught by the sandbox and returned as an error response. Always wrap risky logic in try/catch for clean error messages:
const endpoints = {
'safe-query': async (ctx, req) => {
try {
const rows = await ctx.db.query(
'SELECT COUNT(*) as n FROM heartbeats WHERE user_id = $1',
[ctx.user_id]
);
return { count: rows[0].n };
} catch (e) {
error('Query failed:', e.message);
return { error: 'Could not load data' };
}
}
};
Lifecycle hooks (backend only)
In addition to RPC endpoints, plugins can export a lifecycle object with hooks that run automatically on the server when specific events occur. These hooks have full access to the database and Redis, but run entirely on the backend — they don't affect the frontend.
Available lifecycle events
| Hook | When it runs | Arguments |
|---|---|---|
on_user_login | When a user logs in via OAuth (GitHub/GitLab) | (ctx, event) — event contains user info and login details |
on_heartbeat | After processing each individual heartbeat | (ctx, event) — event contains heartbeat data |
on_heartbeats_bulk | After processing bulk heartbeat imports | (ctx, event) — event contains count of processed heartbeats |
Example lifecycle plugin
// Example: Welcome new users and track first heartbeat
const lifecycle = {
'on_user_login': async (ctx, event) => {
if (event.is_new_user) {
// Send welcome message or set up initial data
await ctx.redis.set('welcome_shown', 'false');
log('Welcome new user:', event.username);
}
},
'on_heartbeat': async (ctx, event) => {
// Track when user sends their first heartbeat
const hasFirstHeartbeat = await ctx.redis.get('has_first_heartbeat');
if (!hasFirstHeartbeat) {
await ctx.redis.set('has_first_heartbeat', 'true');
log('First heartbeat from user:', ctx.user_id, 'project:', event.project);
}
}
};
Lifecycle hooks run in parallel for all installed plugins and are fire-and-forget — they don't return values and errors don't block the main operation. Use them for background processing, logging, notifications, or data aggregation.
// ── on_install: initialize Redis state ───────────────────────────────────────
async function on_install(ctx) {
await ctx.redis.set('streak', '0');
await ctx.redis.set('last-day', '');
log('Streak Notifier installed for', ctx.user_id);
}
// ── on_tick: check daily activity and update streak ───────────────────────────
async function on_tick(ctx) {
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
const lastDay = await ctx.redis.get('last-day');
if (lastDay === today) {
return; // Already checked today
}
const rows = await ctx.db.query(
`SELECT 1 FROM heartbeats
WHERE user_id = $1
AND recorded_at >= NOW() - INTERVAL '1 day'
LIMIT 1`,
[ctx.user_id]
);
if (rows.length > 0) {
const streak = await ctx.redis.incr('streak');
await ctx.redis.set('last-day', today);
log('Day ' + streak + ' of streak — keep it up!');
} else {
warn('No activity today — streak at risk!');
}
}
// ── on_heartbeat: reset risk flag when a heartbeat arrives ───────────────────
async function on_heartbeat(ctx, heartbeat) {
const today = new Date().toISOString().slice(0, 10);
await ctx.redis.set('last-day', today);
}
// ── RPC: widget can call this to display the streak ──────────────────────────
const endpoints = {
'get-streak': async (ctx, req) => {
const streak = await ctx.redis.get('streak');
const lastDay = await ctx.redis.get('last-day');
return {
streak: parseInt(streak || '0', 10),
last_day: lastDay || null
};
},
'reset-streak': async (ctx, req) => {
await ctx.redis.set('streak', '0');
await ctx.redis.set('last-day', '');
log('Streak reset by user', ctx.user_id);
return { ok: true };
}
};
Call GET-streak from your widget plugin to show the live streak count:
// Widget script
fetch('/api/v1/plugins/streak-notifier/rpc/get-streak', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token }
})
.then(r => r.json())
.then(data => {
container.textContent = data.streak + ' day streak 🔥';
})
.catch(() => { container.textContent = '—'; });
Import Data
The POST /api/v1/import endpoint accepts an array of heartbeats in JSON format, allowing you to migrate data from any external tool. Authentication is via API Key or Bearer token.
Request
curl -X POST https://codetrackr.leapcell.app/api/v1/import \
-H "X-API-Key: ct_xyz123" \
-H "Content-Type: application/json" \
-d '[
{
"project": "my-app",
"lang": "rust",
"file": "src/main.rs",
"branch": "main",
"editor": "Windsurf",
"os": "macOS",
"time": 1741276680,
"duration": 60
}
]'
Payload fields
| Field | Required | Description |
|---|---|---|
project |
Yes | Project or workspace name |
lang |
No | Language identifier, e.g. rust, html |
file |
No | Relative file path |
branch |
No | Git branch name |
commit |
No | Git commit SHA |
workspace_root |
No | Git remote URL or workspace root path |
editor |
No | Editor name, e.g. Windsurf, VS Code |
os |
No | Operating system, e.g. macOS, Linux |
machine |
No | Machine identifier |
time |
No | Unix timestamp of the activity. Defaults to current time if omitted |
duration |
No | Duration in seconds. Defaults to 60 |
is_write |
No | Boolean — whether the event was a write action. Defaults to false |
Response
{ "status": "ok", "inserted": 42 }
Notes
- Duplicate entries are silently ignored (
ON CONFLICT DO NOTHING). - You can send up to thousands of records in a single request — batch large imports to avoid timeouts.
- The
timefield accepts Unix timestamps (float or integer). If omitted, the server time is used. - This endpoint is intentionally format-agnostic: write a small script to convert any source (existing trackers, custom logs) into this schema and POST it.
IDE Extensions API
Drop a standard POST payload to the /api/v1/heartbeat endpoint with your API Key to
track activity anywhere.
curl -X POST https://codetrackr.leapcell.app/api/v1/heartbeat \
-H "X-API-Key: ct_xyz123" \
-H "Content-Type: application/json" \
-d '{
"project": "core-engine",
"language": "rust",
"duration": 60
}'
Supported Environments
You can build or use existing wrappers for an extensive list of platforms:
Bulk Pipeline Updates
If your environment caches offline time, you can bulk post logs to /api/v1/heartbeats to
restore histories seamlessly.
Creating IDE Plugins
This guide will help you create a new CodeTrackr plugin for your text editor/IDE. CodeTrackr plugins monitor your coding activity and send heartbeats to the CodeTrackr API to track time spent on projects.
Let us know if you're building a CodeTrackr plugin! We can help you via email and promote your plugin to all CodeTrackr users.
Table of Contents
- Getting Started
- Plugin Overview
- Plugin Initialization
- Listening for Editor Events
- Handling Editor Events
- Sending Heartbeat to CodeTrackr
- Deciding To Send Heartbeat or Not
- Executing Background Process
- Tracking Last Heartbeat
- Debugging
- Releasing
Getting Started
A CodeTrackr plugin is simple… just send a POST request to the CodeTrackr API with the current file details.
curl -X POST https://codetrackr.leapcell.app/api/v1/heartbeat \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"project": "my-project",
"file": "src/main.rs",
"language": "rust",
"duration": 60
}'
The code above sends a heartbeat to the CodeTrackr API and starts logging time for the project.
CodeTrackr plugins have two parts:
- Editor plugin - the core which uses the editor's API and sends the currently focused file to CodeTrackr when the user moves the cursor or types.
- CodeTrackr API - the common API which takes file details as input, detects metadata, and logs the activity.
Plugin Overview
This is a high-level overview of a CodeTrackr plugin from the time it's loaded, until the editor is exited.
- Plugin loaded by text editor/IDE, runs plugin's initialization code
- Initialization code: Setup global variables, check for API key, setup event listeners
- Current file changed: Send heartbeat with is_write=false
- User types in file: Send heartbeat with is_write=false
- File saved: Send heartbeat with is_write=true
- Send heartbeat function: Check last heartbeat, if not too frequent, send to API
Plugin Initialization
This happens every time your plugin is loaded. Check for API key, set up variables, and event listeners.
Examples of init code can be viewed in existing CodeTrackr plugins (e.g., VS Code extension).
Listening for Editor Events
The last step when initializing your plugin is setting up event listeners. Most editors support:
- Currently focused file has changed
- Current file was modified
- A file was saved
Examples of listening for these events in various editors.
Handling Editor Events
Every event handler should call the same function. The File Save event should pass is_write=true.
File Changed
The file changed event detects when the editor focus changes to a new file.
File Modified
The file modified event triggers every time the focused file is changed.
File Saved
The file saved event detects when file contents are written to disk.
Sending Heartbeat to CodeTrackr
Every event handler calls a common function that accepts is_write boolean.
The function detects the current file and sends to CodeTrackr.
Deciding To Send Heartbeat or Not
Check if enough time has passed since last heartbeat for the same file.
if (enoughTimeHasPassed(lastSentTime) || currentlyFocusedFileHasChanged(lastSentFile) || isFileSavedEvent()) {
sendHeartbeatToCodeTrackr();
}
Executing Background Process
Send a POST request to /api/v1/heartbeat with the heartbeat data. Use your API key for authentication.
Fields include: project, file, language, branch, commit, workspace_root, duration, is_write, editor, os, machine, time.
Tracking Last Heartbeat
After sending, update lastSentTime and lastSentFile.
Debugging
Check the dashboard or API for received heartbeats. Use logging in your plugin.
Releasing
Publish your plugin on the editor's marketplace. Send us your repo to promote it.
The Plugin Store
The Plugin Store is a community directory of dashboard plugins. Any authenticated user can publish and install plugins. No server access or recompilation needed.
- Browse: All published plugins are visible on the home page under Plugin Store, no login required.
- Install: One-click install. Installed plugins appear immediately on your dashboard next time you load it.
- Uninstall: Click the
✕button on any panel in your dashboard to remove it. - Privacy: Plugin scripts run entirely in the user's browser. CodeTrackr's servers never make requests to external services on your behalf.
- Moderation: Users can report plugins. Admins can ban or delete them. Banned plugins stop loading for all users immediately.
Publishing to the Store
You must be authenticated via GitHub or GitLab to publish. Use the + Publish Plugin button in your dashboard, or call the API directly:
curl -X POST https://codetrackr.leapcell.app/api/v1/store/publish \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "my-cool-widget",
"display_name": "Project Velocity",
"description": "Visualize your commit velocity over time.",
"version": "1.0.0",
"icon": "📈",
"widget_type": "counter",
"repository": "https://github.com/you/my-cool-widget",
"script": "container.textContent = \"Loading...\"; fetch(\"/api/v1/stats/summary?range=7d\",{headers:{\"Authorization\":\"Bearer \"+token}}).then(r=>r.json()).then(d=>{container.textContent=d.top_project||\"—\";}).catch(()=>{container.textContent=\"—\";});"
}'
Publish fields reference
| Field | Required | Description |
|---|---|---|
name |
Yes | Unique kebab-case identifier, e.g. streak-counter |
display_name |
Yes | Human-readable title shown in the store and dashboard panel header |
description |
No | Short description shown in the store card |
version |
No | Semver string, defaults to 0.1.0 |
icon |
No | Single emoji shown next to the plugin title, defaults to 🔌 |
widget_type |
No | counter · chart · list · text · progress |
repository |
No | URL to the plugin's source code |
script |
No | JavaScript code executed in the user's browser. Receives container (DOM element) and token (Bearer token string) |
Updating an existing plugin
Publishing with the same name performs an in-place update (upsert). All users who have
the plugin installed will receive the new script on their next dashboard load — no reinstall
required.
To update, call the same publish endpoint with the identical name and whatever fields
you want to change:
curl -X POST https://codetrackr.leapcell.app/api/v1/store/publish \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "streak-counter",
"display_name": "Streak Counter",
"version": "1.1.0",
"script": "/* updated script */"
}'
- The
namefield is the unique key — it must match exactly (lowercase kebab-case). - Only the fields you include are updated; omitted optional fields keep their previous values.
- Bump
versionso users can see a changelog in the store card. - Only the original publisher (same authenticated user) can overwrite a plugin.
Themes
The Theme Store lets any authenticated user create, publish, install, and apply CSS variable themes to personalize the CodeTrackr interface. Themes are stored in the database and applied at runtime — no build step required.
- Browse: Open the Plugin Store and click the Themes tab to see all published themes.
- Install: One click to add a theme to your library. Installed themes appear in the Installed themes bar above the editor.
- Apply: Click Apply on any installed theme to activate it. The dashboard repaints instantly via CSS custom properties — no reload needed.
- Customize: Use the built-in CSS variable editor to override any color. Changes preview live before you save.
- Export / Import: Download your current variable set as a
.jsonfile and share it with others, or load someone else's file directly into the editor. - Publish: Share your theme with the community through the + Publish Theme button.
Themes are purely cosmetic — they only set CSS custom properties and an optional raw CSS override block. They have no access to your data or the application logic.
Install a Theme
Go to Plugin Store → Themes. Each card shows a four-color swatch preview. Click Install to add the theme to your library, then Apply to activate it.
To remove a theme from your library, click Uninstall. If the uninstalled theme was active, the dashboard reverts to the default style.
Via the API:
# Install
curl -X POST https://codetrackr.leapcell.app/api/v1/themes/install/THEME_ID \
-H "Authorization: Bearer YOUR_TOKEN"
# Uninstall
curl -X DELETE https://codetrackr.leapcell.app/api/v1/themes/uninstall/THEME_ID \
-H "Authorization: Bearer YOUR_TOKEN"
Customize CSS Variables
The Customize — CSS Variables editor in the Themes tab lets you override any of the ten design tokens that control the interface palette. Changes apply live as you move the color picker or type a value.
| Variable | Controls |
|---|---|
--bg | Page background |
--bg-card | Card and panel background |
--bg-input | Input field background |
--bg-hover | Hover state background |
--text-main | Primary text color |
--text-muted | Secondary / muted text |
--text-dark | Dimmed label text |
--border | Default border color |
--border-focus | Focused / active border |
--accent | Accent / highlight color |
Click Save to persist your overrides to the server. They are stored in user_theme_prefs.custom_vars and merged on top of the active theme every time you load the dashboard. Click Reset to clear all overrides and return to the base theme values.
To apply variables programmatically via the API:
curl -X POST https://codetrackr.leapcell.app/api/v1/themes/apply \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"theme_id": "THEME_UUID_OR_NULL",
"custom_vars": {
"--bg": "#0d0d0d",
"--accent": "#a78bfa"
}
}'
Export & Import
Use Export to download the current editor state as codetrackr-theme.json — a plain JSON object mapping variable names to values:
{
"--bg": "#0d0d0d",
"--bg-card": "#111113",
"--bg-input": "#18181b",
"--bg-hover": "#1e1e22",
"--text-main": "#f4f4f5",
"--text-muted": "#a1a1aa",
"--text-dark": "#52525b",
"--border": "#27272a",
"--border-focus": "#3f3f46",
"--accent": "#a78bfa"
}
Use Import to load a .json file into the editor. The variables are applied as a live preview immediately — click Save to persist them or Reset to discard.
- Only the ten whitelisted variables above are applied. Unknown keys are silently ignored.
- The file must be valid JSON with string values. Any other format shows a warning toast and is rejected.
- You can share theme files directly with other users — they import and apply them with the same flow.
Publish a Theme
Click + Publish Theme in the Themes tab. The form pre-fills the variable fields from the current editor state. Fill in the metadata and click Publish Theme.
Publish fields reference
| Field | Required | Description |
|---|---|---|
name | Yes | Unique kebab-case identifier, e.g. dracula-dark |
display_name | Yes | Human-readable title shown in the store card |
description | No | Short description shown in the store card |
version | No | Semver string, defaults to 1.0.0 |
icon | No | Single emoji, defaults to 🎨 |
variables | Yes | JSON object of CSS variable overrides (see table above) |
custom_css | No | Optional raw CSS string for advanced overrides. Sanitized on the server — @import, url(), and script injection are stripped. |
Publish or update via the API:
curl -X POST https://codetrackr.leapcell.app/api/v1/themes/publish \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "dracula-dark",
"display_name": "Dracula Dark",
"description": "A dark theme inspired by the Dracula palette.",
"version": "1.0.0",
"icon": "🧛",
"variables": {
"--bg": "#282a36",
"--bg-card": "#1e1f29",
"--bg-input": "#21222c",
"--bg-hover": "#44475a",
"--text-main": "#f8f8f2",
"--text-muted": "#6272a4",
"--text-dark": "#44475a",
"--border": "#44475a",
"--border-focus": "#6272a4",
"--accent": "#bd93f9"
}
}'
Publishing with the same name performs an in-place update (upsert). Only the original publisher can overwrite their theme.
Themes API Reference
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/themes | List all published themes in the store |
POST | /api/v1/themes/publish | Publish or update a theme (auth required) |
GET | /api/v1/themes/installed | List themes installed by the authenticated user |
POST | /api/v1/themes/install/:id | Install a theme by UUID |
DELETE | /api/v1/themes/uninstall/:id | Uninstall a theme by UUID |
GET | /api/v1/themes/active | Get the active theme variables merged with custom overrides |
POST | /api/v1/themes/apply | Set active theme and/or custom variable overrides |
GET /api/v1/themes/active — response shape
{
"active_theme_id": "uuid-or-null",
"theme_name": "dracula-dark",
"variables": {
"--bg": "#282a36",
"--accent": "#bd93f9"
}
}
The variables object is the result of merging the active theme's base variables with the user's custom_vars overrides. The dashboard applies these on every page load via document.documentElement.style.setProperty().
Design Tokens
Include the main stylesheet and reference these CSS custom properties in your plugin HTML. All components below depend on them.
<link rel="stylesheet" href="/static/css/main.css" />
--bg#121212--bg-card#18181b--bg-hover#27272a--text-main#ffffff--text-muted#a1a1aa--text-dark#52525b--border#27272a--border-focus#3f3f46Cards
Base container for any plugin panel. Use the .card class directly.
Plugin Title
A short description or metric value goes here.
<div class="card">
<h3>Plugin Title</h3>
<p>A short description or metric value.</p>
</div>
Grid Layout
Use .grid-2 or .grid-3 to lay out multiple cards side by side.
<div class="grid-2">
<div class="card">...</div>
<div class="card">...</div>
</div>
<div class="grid-3">
<div class="card">...</div>
<div class="card">...</div>
<div class="card">...</div>
</div>
Stat Widget
The standard counter card used in the main dashboard. Combines a label, a large mono value, and an optional sub-label.
Today
<div class="card">
<h3 style="font-size:11px; text-transform:uppercase;
color:var(--text-dark); margin-bottom:8px;">
Label
</h3>
<div style="font-size:28px; font-family:var(--font-mono);
color:var(--text-main);">
4h 22m
</div>
<div style="font-size:12px; color:var(--text-muted); margin-top:4px;">
Sub-label
</div>
</div>
Badges & Pills
Small inline labels for status, language, or category. Compose with inline styles on top of
.key-hint.
<!-- Mono badge (keyboard hint style) -->
<span class="key-hint">Rust</span>
<!-- Star / special -->
<span class="key-hint">★</span>
<!-- Live indicator -->
<span style="font-size:10px; color:var(--text-dark);">● Live</span>
Tables
Minimal mono-font table with hover rows. Wrap in .table-wrapper for horizontal scroll
on small viewports.
| Project | Language | Time |
|---|---|---|
| core-engine | Rust | 3h 12m |
| dashboard-ui | TypeScript | 1h 44m |
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Project</th>
<th>Language</th>
<th>Time</th>
</tr>
</thead>
<tbody>
<tr>
<td class="td-main">core-engine</td>
<td>Rust</td>
<td>3h 12m</td>
</tr>
</tbody>
</table>
</div>
Bar Charts
Vertical bar chart used for daily activity. Each bar is a flex column with a percentage height and a
tooltip via title.
<div style="display:flex; align-items:flex-end; gap:6px; height:80px;">
<!-- height% = (minutes / max_minutes) * 100 -->
<div title="Mon · 1h 20m"
style="flex:1; background:var(--border-focus);
border-radius:2px 2px 0 0; height:35%;"></div>
<div title="Tue · 3h 10m"
style="flex:1; background:var(--border-focus);
border-radius:2px 2px 0 0; height:80%;"></div>
<!-- repeat for each day -->
</div>
Language Progress Bars
Horizontal fill bars for language breakdowns inside a card.
<div style="display:flex; flex-direction:column; gap:10px;">
<div>
<div style="display:flex; justify-content:space-between;
font-size:12px; font-family:var(--font-mono); margin-bottom:4px;">
<span style="color:var(--text-main);">Rust</span>
<span style="color:var(--text-dark);">62%</span>
</div>
<div style="height:3px; background:var(--border); border-radius:99px;">
<div style="width:62%; height:100%;
background:var(--border-focus); border-radius:99px;"></div>
</div>
</div>
</div>
Code Blocks
Use a <pre><code> pair. The pre style is defined globally in
main.css — no extra class needed.
<pre><code>your code here</code></pre>
Inline Code
<code>/api/v1/heartbeat</code>
Tab Group + Code Block
Combine .code-tabs and .code-tab above a pre to switch
between snippets.
<div style="border:1px solid var(--border); border-radius:var(--radius); overflow:hidden;">
<div class="code-tabs">
<button class="code-tab active">curl</button>
<button class="code-tab">Python</button>
</div>
<pre style="margin:0; border:none; border-radius:0;">
<code>your snippet</code>
</pre>
</div>
Progress Bars
Horizontal fill bars for progress indicators, file uploads, or import operations inside a widget plugin.
<div style="width:100%; height:6px; background:var(--border);
border-radius:4px; overflow:hidden;">
<div id="progressBar"
style="width:0%; height:100%; background:var(--border-focus);
border-radius:4px; transition:width .2s;"></div>
</div>
Update from JavaScript:
const bar = container.querySelector('#progressBar');
bar.style.width = '64%';
Empty States
Show when a plugin panel has no data yet. Keep it minimal and mono.
<div class="card" style="text-align:center; padding:40px 24px;">
<div style="font-size:24px; margin-bottom:12px;">—</div>
<div style="font-size:12px; color:var(--text-dark);
font-family:var(--font-mono);">
No data yet
</div>
</div>
Loading State
<p style="color:var(--text-muted);">Loading live data...</p>
Forms & Inputs
Use standard HTML inputs styled with the design tokens. All inputs inside your widget plugin inherit the global stylesheet from main.css.
Text input
<input type="text" placeholder="Enter value…"
style="width:100%; background:var(--bg);
border:1px solid var(--border);
border-radius:var(--radius-sm);
padding:6px 10px; font-size:13px;
font-family:var(--font-mono);
color:var(--text-main); outline:none;">
File input
<input type="file" accept=".csv"
style="font-size:12px; font-family:var(--font-mono);
color:var(--text-muted);">
Status label
<div id="status"
style="font-size:12px; color:var(--text-muted);
font-family:var(--font-mono);">
Waiting for file…
</div>
Full widget form example
A complete import widget using progress bar, file input, button, and status label:
container.innerHTML = `
<div style="display:flex; flex-direction:column; gap:10px; max-width:420px;">
<input type="file" id="csvFile" accept=".csv"
style="font-size:12px; font-family:var(--font-mono); color:var(--text-muted);">
<button class="btn" id="importBtn" disabled
style="display:inline-flex; padding:4px 12px; font-size:12px;">
Import CSV
</button>
<div style="width:100%; height:6px; background:var(--border);
border-radius:4px; overflow:hidden;">
<div id="progressBar" style="width:0%; height:100%; background:var(--border-focus);
border-radius:4px; transition:width .2s;"></div>
</div>
<div id="status" style="font-size:12px; color:var(--text-muted);"></div>
</div>
`;
const fileInput = container.querySelector('#csvFile');
const button = container.querySelector('#importBtn');
const status = container.querySelector('#status');
const bar = container.querySelector('#progressBar');
fileInput.onchange = () => {
if (fileInput.files.length) {
button.disabled = false;
status.textContent = 'Ready: ' + fileInput.files[0].name;
}
};
button.onclick = async () => {
status.textContent = 'Uploading…';
// … your fetch logic here …
bar.style.width = '100%';
status.textContent = 'Done!';
};
IDE Color Palette
Developer IDE colors based on each IDE's most dominant icon color.
#fd27bc
#99cd00
#3b8cec
#04dbde
#ec8623
#3ae868
#048184
#49b77e
#0271c6
#fb8007
#067dc3
#fc500c
#8a1922
#288bda
#fdd308
#d97757
#14c9a5
#25a6d9
#3e8e1c
#7368a8
#5a2894
#907cf2
#087cfa
#897363
#db2129
#5c64f4
#443582
#3acca9
#8c76c3
#423f13
#0f753c
#c7b9ff
#d96527
#aca3a4
#fbec75
#872114
#bd4ffc
#1ba334
#35c4c0
#2876e1
#f47822
#dd5f4a
#0b7aef
#fcb414
#a127fc
#2c3494
#12a8d5
#068304
#aed43a
#9ecf54
#ff054a
#4a38a0
#ee848e
#cfcfcf
#ff4119
#d93ac1
#fc6b33
#c6421f
#6a7152
#662d91
#d2ee5c
#323d4f
#B6C2E4
#f7a415
#049cfc
#ff6336
#ff2f52
#1399ef
#da3939
#fdad00
#57ca57
#ffb901
#ff9800
#133f1c
#652d96
#822b7a
#ff4a36
#222d36
#068304
#9460cd
#6185b3
#027acd
#00c6d7
#58e5bc
#b3b3b3
#0f4091
#3598db
#3fa7e4
#0754c6
#5d89af
#d0ce71
#1892e5
#1892e5
#ed3103
#8d76cc
#22a273
#007ac1
#4da0df
#245279
#0a0054
#7fc342
#2369c7
#ee181e
#aeaeae
#fc6143
#c12625
API Reference — Introduction
The CodeTrackr API follows REST conventions. Use GET to retrieve data and POST or DELETE to modify it. All requests must be made over HTTPS.
Every response is a JSON object. Errors are returned under the key error.
Base URL
https://codetrackr.leapcell.app/api/v1
HTTP Status Codes
| Code | Meaning |
|---|---|
200 | OK — request succeeded. |
201 | Created — resource created successfully. |
400 | Bad Request — check the error message and try again. |
401 | Unauthorized — authentication missing or invalid. |
403 | Forbidden — authenticated but not permitted. |
404 | Not Found — the resource does not exist. |
500 | Server Error — try again later. |
Authentication
All authenticated endpoints accept one of the following methods.
API Key (recommended for IDE plugins)
Generate a key from your dashboard and pass it in the X-API-Key header:
X-API-Key: ct_xxxxxxxxxxxxxxxxxxxx
Alternatively, use the query parameter: ?api_key=ct_xxx
Bearer Token (OAuth / JWT)
After logging in via GitHub or GitLab, a JWT is issued. Pass it in the Authorization header:
Authorization: Bearer <your_jwt>
OAuth Login URLs
| Provider | URL |
|---|---|
| GitHub | GET /auth/github |
| GitLab | GET /auth/gitlab |
| Logout | POST /auth/logout |
API Key Limits
Each user may hold up to 5 active API keys. Keys can be managed via the API Keys endpoints or from the dashboard UI. The full key value is only shown once at creation time.
Security
Do not use your API key in public client-side JavaScript. For dashboard widgets use the token argument provided by the plugin runtime, which is always scoped to the authenticated user.
Heartbeat
POST /api/v1/heartbeat
Records a single coding activity event. IDE plugins call this endpoint continuously as you code.
Authentication Required
X-API-Key or Authorization: Bearer
JSON Body
| Field | Type | Required | Description |
|---|---|---|---|
project | string | Yes | Project or workspace name |
file | string | No | Relative file path |
lang | string | No | Language identifier, e.g. rust, typescript |
branch | string | No | Git branch name |
commit | string | No | Git commit SHA |
workspace_root | string | No | Workspace root path or remote URL |
package_path | string | No | Monorepo sub-package path |
duration | integer | No | Duration in seconds (default: 30) |
is_write | boolean | No | Whether the event was a file save (default: false) |
editor | string | No | Editor name, e.g. VS Code, Neovim |
os | string | No | Operating system, e.g. macOS, Linux |
machine | string | No | Machine identifier |
time | float | No | Unix timestamp. Defaults to server time if omitted |
Example Request
curl -X POST https://codetrackr.leapcell.app/api/v1/heartbeat \
-H "X-API-Key: ct_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"project": "core-engine",
"file": "src/main.rs",
"lang": "rust",
"branch": "main",
"editor": "VS Code",
"os": "macOS",
"duration": 60,
"is_write": true
}'
Example Response
Response Code: 200
{
"status": "ok",
"recorded_at": "2026-03-09T19:00:00Z"
}
Heartbeats Bulk
POST /api/v1/heartbeats
Creates multiple heartbeats in a single request. Ideal for offline sync or data imports. Duplicate entries are silently ignored.
Authentication Required
X-API-Key or Authorization: Bearer
JSON Body
An array of heartbeat objects using the same fields as the single Heartbeat endpoint.
Example Request
curl -X POST https://codetrackr.leapcell.app/api/v1/heartbeats \
-H "X-API-Key: ct_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '[
{ "project": "core-engine", "lang": "rust", "duration": 60, "time": 1741276680 },
{ "project": "dashboard-ui", "lang": "typescript", "duration": 120, "time": 1741276800 }
]'
Example Response
Response Code: 200
{
"status": "ok",
"inserted": 2
}
Stats — Summary
GET /api/v1/stats/summary
Aggregate coding stats for the authenticated user over a given time range, including streak info and top language/project.
Authentication Required
X-API-Key or Authorization: Bearer
Query Parameters
| Parameter | Type | Description |
|---|---|---|
range | string | Preset range: 7d (default), 30d, 90d, all |
start | ISO 8601 | Custom range start (overrides range) |
end | ISO 8601 | Custom range end (overrides range) |
Example Response
Response Code: 200
{
"total_seconds": <integer: total seconds in range>,
"daily_average": <integer: average seconds per day>,
"streak_current": <integer: current consecutive coding days>,
"streak_longest": <integer: longest streak ever recorded>,
"top_language": <string|null: most-used language>,
"top_project": <string|null: most-active project>,
"range_start": <string: ISO 8601 UTC start datetime>,
"range_end": <string: ISO 8601 UTC end datetime>
}
Public Platform Stats
GET /api/v1/stats/public — No authentication required.
{
"users": <integer: number of public users>,
"total_seconds": <integer: all-time seconds across all users>
}
Stats — Languages
GET /api/v1/stats/languages
Language breakdown with total seconds and percentage share.
Authentication Required
X-API-Key or Authorization: Bearer
Query Parameters
Same as Stats Summary: range, start, end.
Example Response
Response Code: 200
{
"total_seconds": <integer>,
"languages": [
{
"language": <string: language name>,
"seconds": <integer: total seconds in this language>,
"percentage": <float: percent share of total time, e.g. 62.3>
},
...
]
}
Stats — Projects
GET /api/v1/stats/projects
Time per project with the timestamp of the most recent heartbeat.
Authentication Required
X-API-Key or Authorization: Bearer
Query Parameters
Same as Stats Summary: range, start, end.
Example Response
Response Code: 200
{
"projects": [
{
"project": <string: project name>,
"seconds": <integer: total seconds on this project>,
"last_heartbeat": <string: ISO 8601 UTC datetime of most recent heartbeat>
},
...
]
}
Stats — Daily
GET /api/v1/stats/daily
Per-day seconds array segmented by calendar date. Useful for rendering activity bar charts in plugins.
Authentication Required
X-API-Key or Authorization: Bearer
Query Parameters
Same as Stats Summary: range, start, end.
Example Response
Response Code: 200
{
"daily": [
{
"date": <string: date in YYYY-MM-DD format>,
"seconds": <integer: total seconds on this day>
},
...
]
}
Stats — Streaks
GET /api/v1/stats/streaks
Current and longest consecutive coding day streaks for the authenticated user.
Authentication Required
X-API-Key or Authorization: Bearer
Example Response
Response Code: 200
{
"current_streak": <integer: consecutive days ending today or yesterday>,
"longest_streak": <integer: longest streak ever recorded for this user>
}
Leaderboards
Public leaderboards ranked by coding activity for the current week. No authentication required.
Global Leaderboard
GET /api/v1/leaderboards/global
Query Parameters
| Parameter | Type | Description |
|---|---|---|
limit | integer | Max entries to return (default: 100, max: 500) |
offset | integer | Pagination offset (default: 0) |
country | string | Filter by two-letter country code, e.g. US, MX |
available_for_hire | boolean | When true, only show users available for hire |
Example Response
Response Code: 200
{
"leaderboard": [
{
"rank": <integer: position in leaderboard>,
"user_id": <string: UUID>,
"username": <string: public username>,
"display_name": <string|null>,
"avatar_url": <string|null>,
"country": <string|null: two-letter code>,
"seconds": <integer: total seconds coded this week>,
"top_language": <string|null: most-used language>,
"top_editor": <string|null: most-used editor>,
"top_os": <string|null: most-used operating system>
},
...
],
"week": <string: ISO week identifier, e.g. 2026-W10>,
"updated_at": <string: ISO 8601 UTC datetime>
}
Leaderboard by Language
GET /api/v1/leaderboards/language/:lang
Same structure as global, filtered to users who coded in :lang this week. Accepts limit, offset, country.
Leaderboard by Country
GET /api/v1/leaderboards/country/:country
Delegates to global with a forced country filter. Accepts limit and offset.
Users
Current User
GET /api/v1/user/me
Returns the authenticated user's full private profile.
Authentication Required
X-API-Key or Authorization: Bearer
Example Response
Response Code: 200
{
"id": <string: UUID>,
"username": <string>,
"display_name": <string|null>,
"email": <string|null>,
"avatar_url": <string|null>,
"plan": <string: "free" | "pro">,
"is_public": <boolean>,
"is_admin": <boolean>,
"bio": <string|null>,
"website": <string|null>,
"profile_show_languages": <boolean>,
"profile_show_projects": <boolean>,
"profile_show_activity": <boolean>,
"profile_show_plugins": <boolean>,
"profile_show_streak": <boolean>,
"available_for_hire": <boolean>,
"country": <string|null>,
"timezone": <string: Olson format, e.g. America/Mexico_City>,
"created_at": <string: ISO 8601 UTC datetime>
}
Update Profile
POST /api/v1/user/profile/update
Updates the authenticated user's profile. Only provided fields are changed; omitted fields keep their current values.
Authentication Required
X-API-Key or Authorization: Bearer
JSON Body (all fields optional)
| Field | Type | Description |
|---|---|---|
bio | string | Short user bio |
website | string | Personal website URL (must start with http:// or https://) |
is_public | boolean | Whether profile is publicly visible |
profile_show_languages | boolean | Show languages on public profile |
profile_show_projects | boolean | Show projects on public profile |
profile_show_activity | boolean | Show weekly activity on public profile |
profile_show_plugins | boolean | Show published plugins on public profile |
profile_show_streak | boolean | Show streak on public profile |
available_for_hire | boolean | Display the hireable badge on profile |
Example Response
{ "status": "updated" }
Public Profile
GET /api/v1/user/profile/:username
Returns the public profile of any user with is_public = true. Stats sections respect the user's individual visibility settings.
Example Response
Response Code: 200
{
"username": <string>,
"display_name": <string|null>,
"avatar_url": <string|null>,
"bio": <string|null>,
"website": <string|null>,
"country": <string|null>,
"plan": <string>,
"member_since": <string: ISO 8601 UTC datetime>,
"follower_count": <integer>,
"following_count": <integer>,
"available_for_hire": <boolean>,
"weekly_seconds": <integer: 0 if hidden by user>,
"streak_days": <integer: 0 if hidden by user>,
"languages": <array|[]: top languages if show_languages is true>,
"projects": <array|[]: top projects if show_projects is true>,
"plugins": <array|[]: published plugins if show_plugins is true>
}
Follow / Unfollow
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/user/follow/:username | Follow a user |
DELETE | /api/v1/user/unfollow/:username | Unfollow a user |
GET | /api/v1/user/following/:username | Check if you follow a user |
All three require authentication. You cannot follow yourself — the API returns 400 if attempted.
Following Check Response
{ "following": <boolean> }
SVG Badge
GET /api/v1/user/badge/:username/:lang
Returns an SVG badge showing the user's coding time for a specific language over the last 7 days. Safe to embed in GitHub READMEs — no authentication required.

Contact Developer
POST /api/v1/user/contact/:username
Sends a hire inquiry to a user who has available_for_hire = true and a public profile. The message is stored in the database.
JSON Body
| Field | Required | Description |
|---|---|---|
name | Yes | Sender's name |
email | Yes | Sender's email address |
message | Yes | Message body (max 2000 characters) |
Example Response
{ "status": "sent" }
API Keys
Manage your API keys programmatically. The full key value is only shown once at creation time and cannot be retrieved again.
Limit: 5 keys per user.
List Keys
GET /api/v1/keys
Authentication Required
Authorization: Bearer
Example Response
Response Code: 200
{
"keys": [
{
"id": <string: UUID>,
"name": <string: key label>,
"key_prefix": <string: first 12 characters for identification>,
"last_used_at": <string|null: ISO 8601 UTC datetime>,
"created_at": <string: ISO 8601 UTC datetime>
},
...
]
}
Create Key
POST /api/v1/keys
Authentication Required
Authorization: Bearer
JSON Body
| Field | Required | Description |
|---|---|---|
name | No | Human-readable label. Defaults to Key #N |
Example Response
Response Code: 200
{
"key": {
"id": <string: UUID>,
"name": <string>,
"key": <string: full key — only shown once, store securely>,
"created_at": <string: ISO 8601 UTC datetime>
}
}
Delete Key
DELETE /api/v1/keys/:id
Permanently deletes the API key with the given UUID. Only the owner can delete their own keys.
Authentication Required
Authorization: Bearer
Example Response
{ "message": "API key deleted" }
SVG Badges
Generate embeddable SVG badges for a GitHub README.md or any webpage. Badges require no authentication and no token in the URL — they are driven by the badge-generator plugin and the user's public profile setting.
Get Badge
GET /badge/:username/:metric
Returns an image/svg+xml response. No Authorization header required. If the user's profile is private or the username does not exist, a profile private badge is returned — both cases are intentionally indistinguishable to prevent username enumeration.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
username | string | The public username of the CodeTrackr user. |
metric | string | The badge metric to display (see table below). Only alphanumeric characters, hyphens, and underscores are accepted. |
Query Parameters
| Parameter | Default | Description |
|---|---|---|
style | flat | Badge style forwarded to the plugin. Currently flat is the only built-in style; plugins may support more. |
Available Metrics (badge-generator plugin)
| Metric | Label | Example value | Description |
|---|---|---|---|
coding-time | code time | 14h 32m | Total coding time in the last 7 days. |
today | today | 2h 15m | Coding time since midnight UTC. |
streak | streak | 7 days 🔥 | Current consecutive-days coding streak. |
top-language | language | Rust | Most-used language by seconds in the last 7 days. |
all-time | all time | 1,234h | Total hours coded across all recorded heartbeats. |
Response
Content-Type: image/svg+xml
Cache-Control: max-age=3600, s-maxage=3600 — badges are cached by CDN and browsers for 1 hour.
The SVG follows the Shields.io flat badge format: a dark left pill (label) and a colored right pill (value). Color changes automatically based on activity level.
Embed in a README





Fetch via curl
# Save the SVG locally
curl https://codetrackr.leapcell.app/badge/livrasand/coding-time -o coding-time.svg
# With query params
curl "https://codetrackr.leapcell.app/badge/livrasand/streak?style=flat" -o streak.svg
Privacy & Security
- Public profiles only: Badges only render for users who have
is_public = trueset in their profile settings. Private users receive an identical profile private badge whether the username exists or not. - No token in the URL: Badges never require an API key. This eliminates the risk of leaking credentials through git history or CDN logs.
- Aggregated data only: Badges expose only aggregated statistics (total seconds, streak, top language). Raw file paths, branch names, and commit data are never exposed.
- Rate limited: The
/badge/*route is independently rate-limited at 30 requests/s per IP (burst 60) — separate from the authenticated API limits. - Plugin-driven: The badge logic lives in the
badge-generatorplugin. You can inspect the full source in the Plugin Store. Custom metrics can be added by extending the plugin'sendpointsobject.
Enabling public profile
Badges require the target user's profile to be public. Enable it in the dashboard under Settings → Public profile, or via the API:
curl -X POST https://codetrackr.leapcell.app/api/v1/user/profile/update \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"is_public": true}'
Export
Export your full coding history. Both formats include all heartbeats the authenticated user has recorded.
Export as JSON
GET /api/v1/export/json
Returns a JSON array of all heartbeats. Authentication required.
curl https://codetrackr.leapcell.app/api/v1/export/json \
-H "X-API-Key: ct_xxxxxxxxxxxxxxxxxxxx" \
-o my-data.json
Export as CSV
GET /api/v1/export/csv
Returns a CSV file with one heartbeat per row. Authentication required.
curl https://codetrackr.leapcell.app/api/v1/export/csv \
-H "X-API-Key: ct_xxxxxxxxxxxxxxxxxxxx" \
-o my-data.csv
Billing
Manage the Pro subscription via Stripe. All endpoints require Authorization: Bearer.
Billing Status
GET /api/v1/billing/status
Returns the current subscription plan and expiry date for the authenticated user.
{
"plan": <string: "free" | "pro">,
"plan_expires_at": <string|null: ISO 8601 UTC datetime>,
"stripe_customer_id": <string|null>,
"stripe_subscription_id": <string|null>
}
Create Checkout Session
POST /api/v1/billing/checkout
Creates a Stripe Checkout session and returns the redirect URL to complete payment.
{ "url": <string: Stripe Checkout URL> }
Customer Portal
POST /api/v1/billing/portal
Creates a Stripe Customer Portal session for managing or cancelling the subscription.
{ "url": <string: Stripe Portal URL> }
Billing Config
GET /api/v1/billing/config
Returns the public Stripe publishable key and price metadata needed to render the upgrade UI.