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

  1. Cloudflare Connecting IP header
  2. True Client IP header
  3. X-Forwarded-For (first IP)
  4. X-Real-IP header
  5. X-Client-IP header
  6. Connection IP (fallback)

Rate Limits

Endpoint TypeLimitBurst
General API200 req/s50 burst
Authentication100 req/s20 burst
Webhooks10 req/s5 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

ElementDescription
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 buttonLoads the default plugin template and clears the output.
Clear buttonEmpties the editor and the output pane.
▶ Run buttonExecutes 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:

ModeWhat it does
on_heartbeatCalls your on_heartbeat(ctx, heartbeat) function with a mock heartbeat object. Runs in the QuickJS sandbox on the server.
on_tickCalls your on_tick(ctx) function. Use for scheduled background tasks.
on_installCalls your on_install(ctx) function. Simulates a fresh install.
widgetExecutes 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

ShortcutAction
Cmd/Ctrl + EnterRun 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 — a div DOM 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.

VariableControls
--bgPage background
--bg-cardCard and panel background
--bg-inputInput field background
--bg-hoverHover state background
--text-mainPrimary text color
--text-mutedSecondary / muted text
--text-darkDimmed label text
--borderDefault border color
--border-focusFocused / active border
--accentAccent / 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 token argument 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.

PropertyTypeDescription
ctx.user_idstringUUID 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_urlstringBase 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:

FieldTypeDescription
projectstringProject or workspace name
languagestringProgramming language detected
filestringRelative file path (may be empty)
editorstringEditor name, e.g. VS Code, Neovim
branchstringGit branch (may be empty)
osstringOperating system
machinestringMachine identifier (may be empty)
durationnumberDuration in seconds
timenumberUnix 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:

TableDescription
heartbeatsRaw coding activity events
projectsProject metadata
daily_stats_cachePre-computed daily totals
plugin_storePlugin registry entries
installed_pluginsWhich plugins a user has installed
plugin_settingsPer-user plugin configuration (JSONB)
plugin_reviewsUser-written reviews for plugins
usersUser profile and preferences (read/update own record only)
api_keysUser API keys (manage own keys only)
user_followsFollow 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

LimitValue
Memory16 MB per execution
Stack size512 KB
Execution timeout15 seconds (both lifecycle hooks and RPC handlers)
Network accessNone — fetch, XMLHttpRequest, and sockets are not available
Filesystem accessNone — require, fs, and Node.js APIs are not available
Browser globalsNone — window, document, localStorage, etc. are not available
Allowed SQL commandsSELECT, 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

HookWhen it runsArguments
on_user_loginWhen a user logs in via OAuth (GitHub/GitLab)(ctx, event) — event contains user info and login details
on_heartbeatAfter processing each individual heartbeat(ctx, event) — event contains heartbeat data
on_heartbeats_bulkAfter 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 time field 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:

VS Code
IntelliJ IDEA
Neovim
Vim
Cursor
Windsurf
CLion
WebStorm
Eclipse
Sublime Text
Xcode
Android Studio

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 name field 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 version so 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 .json file 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.

VariableControls
--bgPage background
--bg-cardCard and panel background
--bg-inputInput field background
--bg-hoverHover state background
--text-mainPrimary text color
--text-mutedSecondary / muted text
--text-darkDimmed label text
--borderDefault border color
--border-focusFocused / active border
--accentAccent / 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

FieldRequiredDescription
nameYesUnique kebab-case identifier, e.g. dracula-dark
display_nameYesHuman-readable title shown in the store card
descriptionNoShort description shown in the store card
versionNoSemver string, defaults to 1.0.0
iconNoSingle emoji, defaults to 🎨
variablesYesJSON object of CSS variable overrides (see table above)
custom_cssNoOptional 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

MethodEndpointDescription
GET/api/v1/themesList all published themes in the store
POST/api/v1/themes/publishPublish or update a theme (auth required)
GET/api/v1/themes/installedList themes installed by the authenticated user
POST/api/v1/themes/install/:idInstall a theme by UUID
DELETE/api/v1/themes/uninstall/:idUninstall a theme by UUID
GET/api/v1/themes/activeGet the active theme variables merged with custom overrides
POST/api/v1/themes/applySet 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#3f3f46

Cards

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

4h 22m
Rust
<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>

Buttons

Use the .btn class for any interactive action inside your plugin panel.

<!-- Default -->
<button class="btn">Default</button>

<!-- Outlined -->
<button class="btn" style="border-color:var(--border-focus);">Outlined</button>

<!-- Small -->
<button class="btn" style="padding:4px 10px; font-size:11px;">Small</button>

<!-- Tab group -->
<div style="display:flex; gap:16px;">
  <button class="code-tab active">Tab A</button>
  <button class="code-tab">Tab B</button>
</div>

Badges & Pills

Small inline labels for status, language, or category. Compose with inline styles on top of .key-hint.

Rust Live ● Active
<!-- 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.

Rust 62%
TypeScript 28%
<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.

curl -X POST /api/v1/heartbeat \
  -H "X-API-Key: ct_xyz123"
<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.

Importing… 64%
<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.

No data yet
<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

Loading live data...
<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

Waiting for file…
<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.

Adobe XD
#fd27bc
Android Studio
#99cd00
Antigravity
#3b8cec
AppCode
#04dbde
Aptana
#ec8623
Aqua
#3ae868
Arduino IDE
#048184
Atom
#49b77e
Azure Data Studio
#0271c6
Blender
#fb8007
Brackets
#067dc3
Brave
#fc500c
C++ Builder
#8a1922
Canva
#288bda
Chrome
#fdd308
Claude Code
#d97757
CLion
#14c9a5
Cloud9
#25a6d9
Coda
#3e8e1c
CodeTasty
#7368a8
Cursor
#5a2894
DataGrip
#907cf2
DataSpell
#087cfa
DBeaver
#897363
Delphi
#db2129
Discord
#5c64f4
Eclipse
#443582
Edge
#3acca9
Emacs
#8c76c3
Eric
#423f13
Excel
#0f753c
Figma
#c7b9ff
Firefox
#d96527
Flash Builder
#aca3a4
Geany
#fbec75
Gedit
#872114
GoLand
#bd4ffc
HBuilder X
#1ba334
IDA Pro
#35c4c0
IntelliJ IDEA
#2876e1
Jupyter
#f47822
Kakoune
#dd5f4a
Kate
#0b7aef
Komodo
#fcb414
Maestro
#a127fc
Micro
#2c3494
MPS
#12a8d5
Neovim
#068304
NetBeans
#aed43a
Notepad++
#9ecf54
Nova
#ff054a
Obsidian
#4a38a0
Onivim
#ee848e
OpenCode
#cfcfcf
Oxygen
#ff4119
PhpStorm
#d93ac1
Postman
#fc6b33
PowerPoint
#c6421f
Processing
#6a7152
Pulsar
#662d91
PyCharm
#d2ee5c
Pymakr
#323d4f
ReClassEx
#B6C2E4
Rider
#f7a415
Roblox Studio
#049cfc
RubyMine
#ff6336
RustRover
#ff2f52
Safari
#1399ef
SiYuan
#da3939
Sketch
#fdad00
SlickEdit
#57ca57
SQL Server Management Studio
#ffb901
Sublime Text
#ff9800
Terminal
#133f1c
TeXstudio
#652d96
TextMate
#822b7a
Trae
#ff4a36
Unity
#222d36
Vim
#068304
Visual Studio
#9460cd
Visual Studio for Mac
#6185b3
VS Code
#027acd
WebStorm
#00c6d7
Windsurf
#58e5bc
Wing
#b3b3b3
Word
#0f4091
Xamarin
#3598db
Xcode
#3fa7e4
Zed
#0754c6
Aqua Data Studio
#5d89af
BlueJ
#d0ce71
Code::Blocks
#1892e5
CodeLite
#1892e5
EmEditor
#ed3103
Helix
#8d76cc
KDevelop
#22a273
Light Table
#007ac1
Espresso
#4da0df
MySQL Workbench
#245279
Photoshop
#0a0054
QtCreator
#7fc342
RStudio
#2369c7
Spyder
#ee181e
WebMatrix
#aeaeae
WPS Office
#fc6143
Zotero
#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

CodeMeaning
200OK — request succeeded.
201Created — resource created successfully.
400Bad Request — check the error message and try again.
401Unauthorized — authentication missing or invalid.
403Forbidden — authenticated but not permitted.
404Not Found — the resource does not exist.
500Server 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

ProviderURL
GitHubGET /auth/github
GitLabGET /auth/gitlab
LogoutPOST /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

FieldTypeRequiredDescription
projectstringYesProject or workspace name
filestringNoRelative file path
langstringNoLanguage identifier, e.g. rust, typescript
branchstringNoGit branch name
commitstringNoGit commit SHA
workspace_rootstringNoWorkspace root path or remote URL
package_pathstringNoMonorepo sub-package path
durationintegerNoDuration in seconds (default: 30)
is_writebooleanNoWhether the event was a file save (default: false)
editorstringNoEditor name, e.g. VS Code, Neovim
osstringNoOperating system, e.g. macOS, Linux
machinestringNoMachine identifier
timefloatNoUnix 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

ParameterTypeDescription
rangestringPreset range: 7d (default), 30d, 90d, all
startISO 8601Custom range start (overrides range)
endISO 8601Custom 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

ParameterTypeDescription
limitintegerMax entries to return (default: 100, max: 500)
offsetintegerPagination offset (default: 0)
countrystringFilter by two-letter country code, e.g. US, MX
available_for_hirebooleanWhen 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)

FieldTypeDescription
biostringShort user bio
websitestringPersonal website URL (must start with http:// or https://)
is_publicbooleanWhether profile is publicly visible
profile_show_languagesbooleanShow languages on public profile
profile_show_projectsbooleanShow projects on public profile
profile_show_activitybooleanShow weekly activity on public profile
profile_show_pluginsbooleanShow published plugins on public profile
profile_show_streakbooleanShow streak on public profile
available_for_hirebooleanDisplay 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

MethodEndpointDescription
POST/api/v1/user/follow/:usernameFollow a user
DELETE/api/v1/user/unfollow/:usernameUnfollow a user
GET/api/v1/user/following/:usernameCheck 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.

![CodeTrackr Rust](https://codetrackr.leapcell.app/api/v1/user/badge/yourname/rust)

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

FieldRequiredDescription
nameYesSender's name
emailYesSender's email address
messageYesMessage 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

FieldRequiredDescription
nameNoHuman-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

ParameterTypeDescription
usernamestringThe public username of the CodeTrackr user.
metricstringThe badge metric to display (see table below). Only alphanumeric characters, hyphens, and underscores are accepted.

Query Parameters

ParameterDefaultDescription
styleflatBadge style forwarded to the plugin. Currently flat is the only built-in style; plugins may support more.

Available Metrics (badge-generator plugin)

MetricLabelExample valueDescription
coding-timecode time14h 32mTotal coding time in the last 7 days.
todaytoday2h 15mCoding time since midnight UTC.
streakstreak7 days 🔥Current consecutive-days coding streak.
top-languagelanguageRustMost-used language by seconds in the last 7 days.
all-timeall time1,234hTotal 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

![code time](https://codetrackr.leapcell.app/badge/YOUR_USERNAME/coding-time)
![streak](https://codetrackr.leapcell.app/badge/YOUR_USERNAME/streak)
![top language](https://codetrackr.leapcell.app/badge/YOUR_USERNAME/top-language)
![today](https://codetrackr.leapcell.app/badge/YOUR_USERNAME/today)
![all time](https://codetrackr.leapcell.app/badge/YOUR_USERNAME/all-time)

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 = true set 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-generator plugin. You can inspect the full source in the Plugin Store. Custom metrics can be added by extending the plugin's endpoints object.

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.