Skip to content

Repository files navigation

TaskWorker

A TaskChampion sync server implementation built on Cloudflare Workers and D1.

What is This?

This is a sync server for Taskwarrior 3.0+, implementing the TaskChampion sync protocol. It allows you to synchronize tasks between multiple devices.

Why?

As a general TUI fanboy, I've come to love Taskwarrior. But one of its main pitfalls is hosting the server, so much so that many derivatives have spun up simply because they make the hosting story easier. As a Cloudflare-ain (who has worked on Workers) I knew what needed to be done :)

Architecture

Understanding how TaskChampion sync works is key to understanding this project.

The Two Pieces

┌────────────────────────────────────────────────────────────────┐
│ YOUR DEVICES │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Laptop │ │ Desktop │ │ Phone │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ Task │ │ │ │ Task │ │ │ │ Task │ │ │
│ │ │Warrior│ │ │ │Warrior│ │ │ │Warrior│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │
│ │ │ TC │ │ │ │ TC │ │ │ │ TC │ │ │
│ │ │Library│ │ │ │Library│ │ │ │Library│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ └──────┼──────┘ └──────┼──────┘ └──────┼──────┘ │
│ │ encrypted │ encrypted │ encrypted │
└─────────┼──────────────────┼──────────────────┼────────────────┘
│ │ │
└──────────────────┼──────────────────┘
│
▼
┌───────────────────┐
│ TaskWorker │
│ (this project) │
│ │
│ Stores encrypted │
│ blobs only - │
│ cannot read your │
│ task data │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Cloudflare D1 │
│ (SQLite) │
└───────────────────┘

TaskChampion Library (client-side, in Taskwarrior):

  • Manages your local task database
  • Understands tasks, tags, annotations, dependencies
  • Encrypts all data before sending to the server
  • Decrypts data received from the server

TaskWorker / Sync Server (this project):

  • Stores encrypted binary blobs ("history segments" and "snapshots")
  • Tracks version chains for sync ordering
  • Cannot read your task data - it's encrypted end-to-end
  • Acts as a relay between your devices

Why the Server is "Dumb"

This is a security feature. The server never has access to the encryption key, so even if compromised, your task data remains private. The server only sees opaque binary blobs.

This is why our implementation doesn't need Task, Tag, or Annotation types - we literally cannot read them (intentional per the TaskChampion sync protocol).

Protocol

The sync protocol uses 4 HTTP endpoints:

EndpointMethodPurpose
/v1/client/add-version/:parentVersionIdPOSTSubmit encrypted changes
/v1/client/get-child-version/:parentVersionIdGETFetch next version
/v1/client/add-snapshot/:versionIdPOSTStore encrypted snapshot
/v1/client/snapshotGETFetch latest snapshot

Clients identify themselves via the X-Client-Id header (a UUID shared across your devices).

Deployment

# Install dependencies
npm install
# Create the D1 database
wrangler d1 create taskchampion-sync
# Apply database migrations
wrangler d1 migrations apply taskchampion-sync --remote
# Deploy to Cloudflare Workers
npm run deploy

Your server will be available at https://taskworker.<your-subdomain>.workers.dev.

Usage with Taskwarrior

Prerequisites

  • Taskwarrior 3.0+ (uses TaskChampion sync protocol)
  • A deployed TaskWorker instance (see Deployment above)

Configuration

  1. Generate a client ID (UUID shared across all your devices):

    uuidgen
    # Example output: a1b2c3d4-e5f6-7890-abcd-ef1234567890
  2. Generate an encryption secret (keeps your data private):

    openssl rand -hex 32
    # Example output: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
  3. Configure Taskwarrior by adding to ~/.taskrc:

    # Sync server URL (replace with your deployed URL)sync.server.url=https://taskworker.your-subdomain.workers.dev
    # Client ID (same UUID on all your devices)sync.server.client_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890
    # Encryption secret (same secret on all your devices - keep this private!)sync.encryption_secret=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

    Important: Use the same client_id and encryption_secret on all devices you want to sync.

Syncing

# Sync your tasks
task sync
# First sync on a new device
task sync init

Example Workflow

# On your laptop
task add "Write README" project:docs
task sync
# On your desktop (after initial setup with same credentials)
task sync
task list
# You'll see "Write README" synced from your laptop

Access Control

By default, TaskWorker runs in open mode - any client with a valid UUID can sync. For personal/production use, you should restrict access to only your devices.

Client ID Allowlist

Set the ALLOWED_CLIENT_IDS environment variable to restrict which clients can access your server:

# Using wrangler secret (recommended for production)
wrangler secret put ALLOWED_CLIENT_IDS
# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890# Or for multiple clients (comma-separated)# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890,11111111-2222-3333-4444-555555555555

Alternatively, set it in wrangler.jsonc for development:

{
"vars": {
"ALLOWED_CLIENT_IDS": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}

When the allowlist is configured:

  • Requests from listed client IDs proceed normally
  • Requests from unlisted client IDs receive 403 Forbidden
  • The health check endpoint (/) remains accessible

Security Notes

  • Data is encrypted: Even without access control, your task data is end-to-end encrypted. The server only sees opaque blobs.
  • Client ID as shared secret: Your client ID acts as a weak form of authentication. Keep it private.
  • HTTPS required: Always deploy behind HTTPS (Cloudflare Workers does this automatically).

Development

# Install dependencies
npm install
# Run locally
npm run dev
# Apply database migrations (local)
wrangler d1 migrations apply taskchampion-sync --local
# Run tests
npm test# Lint
npm run lint

Related Projects

License

MIT

About

Taskwarrior server implementation on Cloudflare Workers

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - ndisidore/taskworker: Taskwarrior server implementation on Cloudflare Workers · GitHub
Skip to content

Repository files navigation

TaskWorker

A TaskChampion sync server implementation built on Cloudflare Workers and D1.

What is This?

This is a sync server for Taskwarrior 3.0+, implementing the TaskChampion sync protocol. It allows you to synchronize tasks between multiple devices.

Why?

As a general TUI fanboy, I've come to love Taskwarrior. But one of its main pitfalls is hosting the server, so much so that many derivatives have spun up simply because they make the hosting story easier. As a Cloudflare-ain (who has worked on Workers) I knew what needed to be done :)

Architecture

Understanding how TaskChampion sync works is key to understanding this project.

The Two Pieces

┌────────────────────────────────────────────────────────────────┐
│ YOUR DEVICES │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Laptop │ │ Desktop │ │ Phone │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ Task │ │ │ │ Task │ │ │ │ Task │ │ │
│ │ │Warrior│ │ │ │Warrior│ │ │ │Warrior│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │
│ │ │ TC │ │ │ │ TC │ │ │ │ TC │ │ │
│ │ │Library│ │ │ │Library│ │ │ │Library│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ └──────┼──────┘ └──────┼──────┘ └──────┼──────┘ │
│ │ encrypted │ encrypted │ encrypted │
└─────────┼──────────────────┼──────────────────┼────────────────┘
│ │ │
└──────────────────┼──────────────────┘
│
▼
┌───────────────────┐
│ TaskWorker │
│ (this project) │
│ │
│ Stores encrypted │
│ blobs only - │
│ cannot read your │
│ task data │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Cloudflare D1 │
│ (SQLite) │
└───────────────────┘

TaskChampion Library (client-side, in Taskwarrior):

  • Manages your local task database
  • Understands tasks, tags, annotations, dependencies
  • Encrypts all data before sending to the server
  • Decrypts data received from the server

TaskWorker / Sync Server (this project):

  • Stores encrypted binary blobs ("history segments" and "snapshots")
  • Tracks version chains for sync ordering
  • Cannot read your task data - it's encrypted end-to-end
  • Acts as a relay between your devices

Why the Server is "Dumb"

This is a security feature. The server never has access to the encryption key, so even if compromised, your task data remains private. The server only sees opaque binary blobs.

This is why our implementation doesn't need Task, Tag, or Annotation types - we literally cannot read them (intentional per the TaskChampion sync protocol).

Protocol

The sync protocol uses 4 HTTP endpoints:

EndpointMethodPurpose
/v1/client/add-version/:parentVersionIdPOSTSubmit encrypted changes
/v1/client/get-child-version/:parentVersionIdGETFetch next version
/v1/client/add-snapshot/:versionIdPOSTStore encrypted snapshot
/v1/client/snapshotGETFetch latest snapshot

Clients identify themselves via the X-Client-Id header (a UUID shared across your devices).

Deployment

# Install dependencies
npm install
# Create the D1 database
wrangler d1 create taskchampion-sync
# Apply database migrations
wrangler d1 migrations apply taskchampion-sync --remote
# Deploy to Cloudflare Workers
npm run deploy

Your server will be available at https://taskworker.<your-subdomain>.workers.dev.

Usage with Taskwarrior

Prerequisites

  • Taskwarrior 3.0+ (uses TaskChampion sync protocol)
  • A deployed TaskWorker instance (see Deployment above)

Configuration

  1. Generate a client ID (UUID shared across all your devices):

    uuidgen
    # Example output: a1b2c3d4-e5f6-7890-abcd-ef1234567890
  2. Generate an encryption secret (keeps your data private):

    openssl rand -hex 32
    # Example output: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
  3. Configure Taskwarrior by adding to ~/.taskrc:

    # Sync server URL (replace with your deployed URL)sync.server.url=https://taskworker.your-subdomain.workers.dev
    # Client ID (same UUID on all your devices)sync.server.client_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890
    # Encryption secret (same secret on all your devices - keep this private!)sync.encryption_secret=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

    Important: Use the same client_id and encryption_secret on all devices you want to sync.

Syncing

# Sync your tasks
task sync
# First sync on a new device
task sync init

Example Workflow

# On your laptop
task add "Write README" project:docs
task sync
# On your desktop (after initial setup with same credentials)
task sync
task list
# You'll see "Write README" synced from your laptop

Access Control

By default, TaskWorker runs in open mode - any client with a valid UUID can sync. For personal/production use, you should restrict access to only your devices.

Client ID Allowlist

Set the ALLOWED_CLIENT_IDS environment variable to restrict which clients can access your server:

# Using wrangler secret (recommended for production)
wrangler secret put ALLOWED_CLIENT_IDS
# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890# Or for multiple clients (comma-separated)# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890,11111111-2222-3333-4444-555555555555

Alternatively, set it in wrangler.jsonc for development:

{
"vars": {
"ALLOWED_CLIENT_IDS": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}

When the allowlist is configured:

  • Requests from listed client IDs proceed normally
  • Requests from unlisted client IDs receive 403 Forbidden
  • The health check endpoint (/) remains accessible

Security Notes

  • Data is encrypted: Even without access control, your task data is end-to-end encrypted. The server only sees opaque blobs.
  • Client ID as shared secret: Your client ID acts as a weak form of authentication. Keep it private.
  • HTTPS required: Always deploy behind HTTPS (Cloudflare Workers does this automatically).

Development

# Install dependencies
npm install
# Run locally
npm run dev
# Apply database migrations (local)
wrangler d1 migrations apply taskchampion-sync --local
# Run tests
npm test# Lint
npm run lint

Related Projects

License

MIT

About

Taskwarrior server implementation on Cloudflare Workers

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - ndisidore/taskworker: Taskwarrior server implementation on Cloudflare Workers · GitHub
Skip to content

Repository files navigation

TaskWorker

A TaskChampion sync server implementation built on Cloudflare Workers and D1.

What is This?

This is a sync server for Taskwarrior 3.0+, implementing the TaskChampion sync protocol. It allows you to synchronize tasks between multiple devices.

Why?

As a general TUI fanboy, I've come to love Taskwarrior. But one of its main pitfalls is hosting the server, so much so that many derivatives have spun up simply because they make the hosting story easier. As a Cloudflare-ain (who has worked on Workers) I knew what needed to be done :)

Architecture

Understanding how TaskChampion sync works is key to understanding this project.

The Two Pieces

┌────────────────────────────────────────────────────────────────┐
│ YOUR DEVICES │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Laptop │ │ Desktop │ │ Phone │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ Task │ │ │ │ Task │ │ │ │ Task │ │ │
│ │ │Warrior│ │ │ │Warrior│ │ │ │Warrior│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │
│ │ │ TC │ │ │ │ TC │ │ │ │ TC │ │ │
│ │ │Library│ │ │ │Library│ │ │ │Library│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ └──────┼──────┘ └──────┼──────┘ └──────┼──────┘ │
│ │ encrypted │ encrypted │ encrypted │
└─────────┼──────────────────┼──────────────────┼────────────────┘
│ │ │
└──────────────────┼──────────────────┘
│
▼
┌───────────────────┐
│ TaskWorker │
│ (this project) │
│ │
│ Stores encrypted │
│ blobs only - │
│ cannot read your │
│ task data │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Cloudflare D1 │
│ (SQLite) │
└───────────────────┘

TaskChampion Library (client-side, in Taskwarrior):

  • Manages your local task database
  • Understands tasks, tags, annotations, dependencies
  • Encrypts all data before sending to the server
  • Decrypts data received from the server

TaskWorker / Sync Server (this project):

  • Stores encrypted binary blobs ("history segments" and "snapshots")
  • Tracks version chains for sync ordering
  • Cannot read your task data - it's encrypted end-to-end
  • Acts as a relay between your devices

Why the Server is "Dumb"

This is a security feature. The server never has access to the encryption key, so even if compromised, your task data remains private. The server only sees opaque binary blobs.

This is why our implementation doesn't need Task, Tag, or Annotation types - we literally cannot read them (intentional per the TaskChampion sync protocol).

Protocol

The sync protocol uses 4 HTTP endpoints:

EndpointMethodPurpose
/v1/client/add-version/:parentVersionIdPOSTSubmit encrypted changes
/v1/client/get-child-version/:parentVersionIdGETFetch next version
/v1/client/add-snapshot/:versionIdPOSTStore encrypted snapshot
/v1/client/snapshotGETFetch latest snapshot

Clients identify themselves via the X-Client-Id header (a UUID shared across your devices).

Deployment

# Install dependencies
npm install
# Create the D1 database
wrangler d1 create taskchampion-sync
# Apply database migrations
wrangler d1 migrations apply taskchampion-sync --remote
# Deploy to Cloudflare Workers
npm run deploy

Your server will be available at https://taskworker.<your-subdomain>.workers.dev.

Usage with Taskwarrior

Prerequisites

  • Taskwarrior 3.0+ (uses TaskChampion sync protocol)
  • A deployed TaskWorker instance (see Deployment above)

Configuration

  1. Generate a client ID (UUID shared across all your devices):

    uuidgen
    # Example output: a1b2c3d4-e5f6-7890-abcd-ef1234567890
  2. Generate an encryption secret (keeps your data private):

    openssl rand -hex 32
    # Example output: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
  3. Configure Taskwarrior by adding to ~/.taskrc:

    # Sync server URL (replace with your deployed URL)sync.server.url=https://taskworker.your-subdomain.workers.dev
    # Client ID (same UUID on all your devices)sync.server.client_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890
    # Encryption secret (same secret on all your devices - keep this private!)sync.encryption_secret=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

    Important: Use the same client_id and encryption_secret on all devices you want to sync.

Syncing

# Sync your tasks
task sync
# First sync on a new device
task sync init

Example Workflow

# On your laptop
task add "Write README" project:docs
task sync
# On your desktop (after initial setup with same credentials)
task sync
task list
# You'll see "Write README" synced from your laptop

Access Control

By default, TaskWorker runs in open mode - any client with a valid UUID can sync. For personal/production use, you should restrict access to only your devices.

Client ID Allowlist

Set the ALLOWED_CLIENT_IDS environment variable to restrict which clients can access your server:

# Using wrangler secret (recommended for production)
wrangler secret put ALLOWED_CLIENT_IDS
# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890# Or for multiple clients (comma-separated)# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890,11111111-2222-3333-4444-555555555555

Alternatively, set it in wrangler.jsonc for development:

{
"vars": {
"ALLOWED_CLIENT_IDS": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}

When the allowlist is configured:

  • Requests from listed client IDs proceed normally
  • Requests from unlisted client IDs receive 403 Forbidden
  • The health check endpoint (/) remains accessible

Security Notes

  • Data is encrypted: Even without access control, your task data is end-to-end encrypted. The server only sees opaque blobs.
  • Client ID as shared secret: Your client ID acts as a weak form of authentication. Keep it private.
  • HTTPS required: Always deploy behind HTTPS (Cloudflare Workers does this automatically).

Development

# Install dependencies
npm install
# Run locally
npm run dev
# Apply database migrations (local)
wrangler d1 migrations apply taskchampion-sync --local
# Run tests
npm test# Lint
npm run lint

Related Projects

License

MIT

About

Taskwarrior server implementation on Cloudflare Workers

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - ndisidore/taskworker: Taskwarrior server implementation on Cloudflare Workers · GitHub
Skip to content

Repository files navigation

TaskWorker

A TaskChampion sync server implementation built on Cloudflare Workers and D1.

What is This?

This is a sync server for Taskwarrior 3.0+, implementing the TaskChampion sync protocol. It allows you to synchronize tasks between multiple devices.

Why?

As a general TUI fanboy, I've come to love Taskwarrior. But one of its main pitfalls is hosting the server, so much so that many derivatives have spun up simply because they make the hosting story easier. As a Cloudflare-ain (who has worked on Workers) I knew what needed to be done :)

Architecture

Understanding how TaskChampion sync works is key to understanding this project.

The Two Pieces

┌────────────────────────────────────────────────────────────────┐
│ YOUR DEVICES │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Laptop │ │ Desktop │ │ Phone │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ Task │ │ │ │ Task │ │ │ │ Task │ │ │
│ │ │Warrior│ │ │ │Warrior│ │ │ │Warrior│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │
│ │ │ TC │ │ │ │ TC │ │ │ │ TC │ │ │
│ │ │Library│ │ │ │Library│ │ │ │Library│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ └──────┼──────┘ └──────┼──────┘ └──────┼──────┘ │
│ │ encrypted │ encrypted │ encrypted │
└─────────┼──────────────────┼──────────────────┼────────────────┘
│ │ │
└──────────────────┼──────────────────┘
│
▼
┌───────────────────┐
│ TaskWorker │
│ (this project) │
│ │
│ Stores encrypted │
│ blobs only - │
│ cannot read your │
│ task data │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Cloudflare D1 │
│ (SQLite) │
└───────────────────┘

TaskChampion Library (client-side, in Taskwarrior):

  • Manages your local task database
  • Understands tasks, tags, annotations, dependencies
  • Encrypts all data before sending to the server
  • Decrypts data received from the server

TaskWorker / Sync Server (this project):

  • Stores encrypted binary blobs ("history segments" and "snapshots")
  • Tracks version chains for sync ordering
  • Cannot read your task data - it's encrypted end-to-end
  • Acts as a relay between your devices

Why the Server is "Dumb"

This is a security feature. The server never has access to the encryption key, so even if compromised, your task data remains private. The server only sees opaque binary blobs.

This is why our implementation doesn't need Task, Tag, or Annotation types - we literally cannot read them (intentional per the TaskChampion sync protocol).

Protocol

The sync protocol uses 4 HTTP endpoints:

EndpointMethodPurpose
/v1/client/add-version/:parentVersionIdPOSTSubmit encrypted changes
/v1/client/get-child-version/:parentVersionIdGETFetch next version
/v1/client/add-snapshot/:versionIdPOSTStore encrypted snapshot
/v1/client/snapshotGETFetch latest snapshot

Clients identify themselves via the X-Client-Id header (a UUID shared across your devices).

Deployment

# Install dependencies
npm install
# Create the D1 database
wrangler d1 create taskchampion-sync
# Apply database migrations
wrangler d1 migrations apply taskchampion-sync --remote
# Deploy to Cloudflare Workers
npm run deploy

Your server will be available at https://taskworker.<your-subdomain>.workers.dev.

Usage with Taskwarrior

Prerequisites

  • Taskwarrior 3.0+ (uses TaskChampion sync protocol)
  • A deployed TaskWorker instance (see Deployment above)

Configuration

  1. Generate a client ID (UUID shared across all your devices):

    uuidgen
    # Example output: a1b2c3d4-e5f6-7890-abcd-ef1234567890
  2. Generate an encryption secret (keeps your data private):

    openssl rand -hex 32
    # Example output: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
  3. Configure Taskwarrior by adding to ~/.taskrc:

    # Sync server URL (replace with your deployed URL)sync.server.url=https://taskworker.your-subdomain.workers.dev
    # Client ID (same UUID on all your devices)sync.server.client_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890
    # Encryption secret (same secret on all your devices - keep this private!)sync.encryption_secret=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

    Important: Use the same client_id and encryption_secret on all devices you want to sync.

Syncing

# Sync your tasks
task sync
# First sync on a new device
task sync init

Example Workflow

# On your laptop
task add "Write README" project:docs
task sync
# On your desktop (after initial setup with same credentials)
task sync
task list
# You'll see "Write README" synced from your laptop

Access Control

By default, TaskWorker runs in open mode - any client with a valid UUID can sync. For personal/production use, you should restrict access to only your devices.

Client ID Allowlist

Set the ALLOWED_CLIENT_IDS environment variable to restrict which clients can access your server:

# Using wrangler secret (recommended for production)
wrangler secret put ALLOWED_CLIENT_IDS
# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890# Or for multiple clients (comma-separated)# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890,11111111-2222-3333-4444-555555555555

Alternatively, set it in wrangler.jsonc for development:

{
"vars": {
"ALLOWED_CLIENT_IDS": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}

When the allowlist is configured:

  • Requests from listed client IDs proceed normally
  • Requests from unlisted client IDs receive 403 Forbidden
  • The health check endpoint (/) remains accessible

Security Notes

  • Data is encrypted: Even without access control, your task data is end-to-end encrypted. The server only sees opaque blobs.
  • Client ID as shared secret: Your client ID acts as a weak form of authentication. Keep it private.
  • HTTPS required: Always deploy behind HTTPS (Cloudflare Workers does this automatically).

Development

# Install dependencies
npm install
# Run locally
npm run dev
# Apply database migrations (local)
wrangler d1 migrations apply taskchampion-sync --local
# Run tests
npm test# Lint
npm run lint

Related Projects

License

MIT

About

Taskwarrior server implementation on Cloudflare Workers

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - ndisidore/taskworker: Taskwarrior server implementation on Cloudflare Workers · GitHub
Skip to content

Repository files navigation

TaskWorker

A TaskChampion sync server implementation built on Cloudflare Workers and D1.

What is This?

This is a sync server for Taskwarrior 3.0+, implementing the TaskChampion sync protocol. It allows you to synchronize tasks between multiple devices.

Why?

As a general TUI fanboy, I've come to love Taskwarrior. But one of its main pitfalls is hosting the server, so much so that many derivatives have spun up simply because they make the hosting story easier. As a Cloudflare-ain (who has worked on Workers) I knew what needed to be done :)

Architecture

Understanding how TaskChampion sync works is key to understanding this project.

The Two Pieces

┌────────────────────────────────────────────────────────────────┐
│ YOUR DEVICES │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Laptop │ │ Desktop │ │ Phone │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ Task │ │ │ │ Task │ │ │ │ Task │ │ │
│ │ │Warrior│ │ │ │Warrior│ │ │ │Warrior│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │
│ │ │ TC │ │ │ │ TC │ │ │ │ TC │ │ │
│ │ │Library│ │ │ │Library│ │ │ │Library│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ └──────┼──────┘ └──────┼──────┘ └──────┼──────┘ │
│ │ encrypted │ encrypted │ encrypted │
└─────────┼──────────────────┼──────────────────┼────────────────┘
│ │ │
└──────────────────┼──────────────────┘
│
▼
┌───────────────────┐
│ TaskWorker │
│ (this project) │
│ │
│ Stores encrypted │
│ blobs only - │
│ cannot read your │
│ task data │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Cloudflare D1 │
│ (SQLite) │
└───────────────────┘

TaskChampion Library (client-side, in Taskwarrior):

  • Manages your local task database
  • Understands tasks, tags, annotations, dependencies
  • Encrypts all data before sending to the server
  • Decrypts data received from the server

TaskWorker / Sync Server (this project):

  • Stores encrypted binary blobs ("history segments" and "snapshots")
  • Tracks version chains for sync ordering
  • Cannot read your task data - it's encrypted end-to-end
  • Acts as a relay between your devices

Why the Server is "Dumb"

This is a security feature. The server never has access to the encryption key, so even if compromised, your task data remains private. The server only sees opaque binary blobs.

This is why our implementation doesn't need Task, Tag, or Annotation types - we literally cannot read them (intentional per the TaskChampion sync protocol).

Protocol

The sync protocol uses 4 HTTP endpoints:

EndpointMethodPurpose
/v1/client/add-version/:parentVersionIdPOSTSubmit encrypted changes
/v1/client/get-child-version/:parentVersionIdGETFetch next version
/v1/client/add-snapshot/:versionIdPOSTStore encrypted snapshot
/v1/client/snapshotGETFetch latest snapshot

Clients identify themselves via the X-Client-Id header (a UUID shared across your devices).

Deployment

# Install dependencies
npm install
# Create the D1 database
wrangler d1 create taskchampion-sync
# Apply database migrations
wrangler d1 migrations apply taskchampion-sync --remote
# Deploy to Cloudflare Workers
npm run deploy

Your server will be available at https://taskworker.<your-subdomain>.workers.dev.

Usage with Taskwarrior

Prerequisites

  • Taskwarrior 3.0+ (uses TaskChampion sync protocol)
  • A deployed TaskWorker instance (see Deployment above)

Configuration

  1. Generate a client ID (UUID shared across all your devices):

    uuidgen
    # Example output: a1b2c3d4-e5f6-7890-abcd-ef1234567890
  2. Generate an encryption secret (keeps your data private):

    openssl rand -hex 32
    # Example output: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
  3. Configure Taskwarrior by adding to ~/.taskrc:

    # Sync server URL (replace with your deployed URL)sync.server.url=https://taskworker.your-subdomain.workers.dev
    # Client ID (same UUID on all your devices)sync.server.client_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890
    # Encryption secret (same secret on all your devices - keep this private!)sync.encryption_secret=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

    Important: Use the same client_id and encryption_secret on all devices you want to sync.

Syncing

# Sync your tasks
task sync
# First sync on a new device
task sync init

Example Workflow

# On your laptop
task add "Write README" project:docs
task sync
# On your desktop (after initial setup with same credentials)
task sync
task list
# You'll see "Write README" synced from your laptop

Access Control

By default, TaskWorker runs in open mode - any client with a valid UUID can sync. For personal/production use, you should restrict access to only your devices.

Client ID Allowlist

Set the ALLOWED_CLIENT_IDS environment variable to restrict which clients can access your server:

# Using wrangler secret (recommended for production)
wrangler secret put ALLOWED_CLIENT_IDS
# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890# Or for multiple clients (comma-separated)# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890,11111111-2222-3333-4444-555555555555

Alternatively, set it in wrangler.jsonc for development:

{
"vars": {
"ALLOWED_CLIENT_IDS": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}

When the allowlist is configured:

  • Requests from listed client IDs proceed normally
  • Requests from unlisted client IDs receive 403 Forbidden
  • The health check endpoint (/) remains accessible

Security Notes

  • Data is encrypted: Even without access control, your task data is end-to-end encrypted. The server only sees opaque blobs.
  • Client ID as shared secret: Your client ID acts as a weak form of authentication. Keep it private.
  • HTTPS required: Always deploy behind HTTPS (Cloudflare Workers does this automatically).

Development

# Install dependencies
npm install
# Run locally
npm run dev
# Apply database migrations (local)
wrangler d1 migrations apply taskchampion-sync --local
# Run tests
npm test# Lint
npm run lint

Related Projects

License

MIT

About

Taskwarrior server implementation on Cloudflare Workers

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - ndisidore/taskworker: Taskwarrior server implementation on Cloudflare Workers · GitHub
Skip to content

Repository files navigation

TaskWorker

A TaskChampion sync server implementation built on Cloudflare Workers and D1.

What is This?

This is a sync server for Taskwarrior 3.0+, implementing the TaskChampion sync protocol. It allows you to synchronize tasks between multiple devices.

Why?

As a general TUI fanboy, I've come to love Taskwarrior. But one of its main pitfalls is hosting the server, so much so that many derivatives have spun up simply because they make the hosting story easier. As a Cloudflare-ain (who has worked on Workers) I knew what needed to be done :)

Architecture

Understanding how TaskChampion sync works is key to understanding this project.

The Two Pieces

┌────────────────────────────────────────────────────────────────┐
│ YOUR DEVICES │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Laptop │ │ Desktop │ │ Phone │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ Task │ │ │ │ Task │ │ │ │ Task │ │ │
│ │ │Warrior│ │ │ │Warrior│ │ │ │Warrior│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │
│ │ │ TC │ │ │ │ TC │ │ │ │ TC │ │ │
│ │ │Library│ │ │ │Library│ │ │ │Library│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ └──────┼──────┘ └──────┼──────┘ └──────┼──────┘ │
│ │ encrypted │ encrypted │ encrypted │
└─────────┼──────────────────┼──────────────────┼────────────────┘
│ │ │
└──────────────────┼──────────────────┘
│
▼
┌───────────────────┐
│ TaskWorker │
│ (this project) │
│ │
│ Stores encrypted │
│ blobs only - │
│ cannot read your │
│ task data │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Cloudflare D1 │
│ (SQLite) │
└───────────────────┘

TaskChampion Library (client-side, in Taskwarrior):

  • Manages your local task database
  • Understands tasks, tags, annotations, dependencies
  • Encrypts all data before sending to the server
  • Decrypts data received from the server

TaskWorker / Sync Server (this project):

  • Stores encrypted binary blobs ("history segments" and "snapshots")
  • Tracks version chains for sync ordering
  • Cannot read your task data - it's encrypted end-to-end
  • Acts as a relay between your devices

Why the Server is "Dumb"

This is a security feature. The server never has access to the encryption key, so even if compromised, your task data remains private. The server only sees opaque binary blobs.

This is why our implementation doesn't need Task, Tag, or Annotation types - we literally cannot read them (intentional per the TaskChampion sync protocol).

Protocol

The sync protocol uses 4 HTTP endpoints:

EndpointMethodPurpose
/v1/client/add-version/:parentVersionIdPOSTSubmit encrypted changes
/v1/client/get-child-version/:parentVersionIdGETFetch next version
/v1/client/add-snapshot/:versionIdPOSTStore encrypted snapshot
/v1/client/snapshotGETFetch latest snapshot

Clients identify themselves via the X-Client-Id header (a UUID shared across your devices).

Deployment

# Install dependencies
npm install
# Create the D1 database
wrangler d1 create taskchampion-sync
# Apply database migrations
wrangler d1 migrations apply taskchampion-sync --remote
# Deploy to Cloudflare Workers
npm run deploy

Your server will be available at https://taskworker.<your-subdomain>.workers.dev.

Usage with Taskwarrior

Prerequisites

  • Taskwarrior 3.0+ (uses TaskChampion sync protocol)
  • A deployed TaskWorker instance (see Deployment above)

Configuration

  1. Generate a client ID (UUID shared across all your devices):

    uuidgen
    # Example output: a1b2c3d4-e5f6-7890-abcd-ef1234567890
  2. Generate an encryption secret (keeps your data private):

    openssl rand -hex 32
    # Example output: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
  3. Configure Taskwarrior by adding to ~/.taskrc:

    # Sync server URL (replace with your deployed URL)sync.server.url=https://taskworker.your-subdomain.workers.dev
    # Client ID (same UUID on all your devices)sync.server.client_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890
    # Encryption secret (same secret on all your devices - keep this private!)sync.encryption_secret=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

    Important: Use the same client_id and encryption_secret on all devices you want to sync.

Syncing

# Sync your tasks
task sync
# First sync on a new device
task sync init

Example Workflow

# On your laptop
task add "Write README" project:docs
task sync
# On your desktop (after initial setup with same credentials)
task sync
task list
# You'll see "Write README" synced from your laptop

Access Control

By default, TaskWorker runs in open mode - any client with a valid UUID can sync. For personal/production use, you should restrict access to only your devices.

Client ID Allowlist

Set the ALLOWED_CLIENT_IDS environment variable to restrict which clients can access your server:

# Using wrangler secret (recommended for production)
wrangler secret put ALLOWED_CLIENT_IDS
# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890# Or for multiple clients (comma-separated)# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890,11111111-2222-3333-4444-555555555555

Alternatively, set it in wrangler.jsonc for development:

{
"vars": {
"ALLOWED_CLIENT_IDS": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}

When the allowlist is configured:

  • Requests from listed client IDs proceed normally
  • Requests from unlisted client IDs receive 403 Forbidden
  • The health check endpoint (/) remains accessible

Security Notes

  • Data is encrypted: Even without access control, your task data is end-to-end encrypted. The server only sees opaque blobs.
  • Client ID as shared secret: Your client ID acts as a weak form of authentication. Keep it private.
  • HTTPS required: Always deploy behind HTTPS (Cloudflare Workers does this automatically).

Development

# Install dependencies
npm install
# Run locally
npm run dev
# Apply database migrations (local)
wrangler d1 migrations apply taskchampion-sync --local
# Run tests
npm test# Lint
npm run lint

Related Projects

License

MIT

About

Taskwarrior server implementation on Cloudflare Workers

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - ndisidore/taskworker: Taskwarrior server implementation on Cloudflare Workers · GitHub
Skip to content

Repository files navigation

TaskWorker

A TaskChampion sync server implementation built on Cloudflare Workers and D1.

What is This?

This is a sync server for Taskwarrior 3.0+, implementing the TaskChampion sync protocol. It allows you to synchronize tasks between multiple devices.

Why?

As a general TUI fanboy, I've come to love Taskwarrior. But one of its main pitfalls is hosting the server, so much so that many derivatives have spun up simply because they make the hosting story easier. As a Cloudflare-ain (who has worked on Workers) I knew what needed to be done :)

Architecture

Understanding how TaskChampion sync works is key to understanding this project.

The Two Pieces

┌────────────────────────────────────────────────────────────────┐
│ YOUR DEVICES │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Laptop │ │ Desktop │ │ Phone │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ Task │ │ │ │ Task │ │ │ │ Task │ │ │
│ │ │Warrior│ │ │ │Warrior│ │ │ │Warrior│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │
│ │ │ TC │ │ │ │ TC │ │ │ │ TC │ │ │
│ │ │Library│ │ │ │Library│ │ │ │Library│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ └──────┼──────┘ └──────┼──────┘ └──────┼──────┘ │
│ │ encrypted │ encrypted │ encrypted │
└─────────┼──────────────────┼──────────────────┼────────────────┘
│ │ │
└──────────────────┼──────────────────┘
│
▼
┌───────────────────┐
│ TaskWorker │
│ (this project) │
│ │
│ Stores encrypted │
│ blobs only - │
│ cannot read your │
│ task data │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Cloudflare D1 │
│ (SQLite) │
└───────────────────┘

TaskChampion Library (client-side, in Taskwarrior):

  • Manages your local task database
  • Understands tasks, tags, annotations, dependencies
  • Encrypts all data before sending to the server
  • Decrypts data received from the server

TaskWorker / Sync Server (this project):

  • Stores encrypted binary blobs ("history segments" and "snapshots")
  • Tracks version chains for sync ordering
  • Cannot read your task data - it's encrypted end-to-end
  • Acts as a relay between your devices

Why the Server is "Dumb"

This is a security feature. The server never has access to the encryption key, so even if compromised, your task data remains private. The server only sees opaque binary blobs.

This is why our implementation doesn't need Task, Tag, or Annotation types - we literally cannot read them (intentional per the TaskChampion sync protocol).

Protocol

The sync protocol uses 4 HTTP endpoints:

EndpointMethodPurpose
/v1/client/add-version/:parentVersionIdPOSTSubmit encrypted changes
/v1/client/get-child-version/:parentVersionIdGETFetch next version
/v1/client/add-snapshot/:versionIdPOSTStore encrypted snapshot
/v1/client/snapshotGETFetch latest snapshot

Clients identify themselves via the X-Client-Id header (a UUID shared across your devices).

Deployment

# Install dependencies
npm install
# Create the D1 database
wrangler d1 create taskchampion-sync
# Apply database migrations
wrangler d1 migrations apply taskchampion-sync --remote
# Deploy to Cloudflare Workers
npm run deploy

Your server will be available at https://taskworker.<your-subdomain>.workers.dev.

Usage with Taskwarrior

Prerequisites

  • Taskwarrior 3.0+ (uses TaskChampion sync protocol)
  • A deployed TaskWorker instance (see Deployment above)

Configuration

  1. Generate a client ID (UUID shared across all your devices):

    uuidgen
    # Example output: a1b2c3d4-e5f6-7890-abcd-ef1234567890
  2. Generate an encryption secret (keeps your data private):

    openssl rand -hex 32
    # Example output: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
  3. Configure Taskwarrior by adding to ~/.taskrc:

    # Sync server URL (replace with your deployed URL)sync.server.url=https://taskworker.your-subdomain.workers.dev
    # Client ID (same UUID on all your devices)sync.server.client_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890
    # Encryption secret (same secret on all your devices - keep this private!)sync.encryption_secret=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

    Important: Use the same client_id and encryption_secret on all devices you want to sync.

Syncing

# Sync your tasks
task sync
# First sync on a new device
task sync init

Example Workflow

# On your laptop
task add "Write README" project:docs
task sync
# On your desktop (after initial setup with same credentials)
task sync
task list
# You'll see "Write README" synced from your laptop

Access Control

By default, TaskWorker runs in open mode - any client with a valid UUID can sync. For personal/production use, you should restrict access to only your devices.

Client ID Allowlist

Set the ALLOWED_CLIENT_IDS environment variable to restrict which clients can access your server:

# Using wrangler secret (recommended for production)
wrangler secret put ALLOWED_CLIENT_IDS
# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890# Or for multiple clients (comma-separated)# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890,11111111-2222-3333-4444-555555555555

Alternatively, set it in wrangler.jsonc for development:

{
"vars": {
"ALLOWED_CLIENT_IDS": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}

When the allowlist is configured:

  • Requests from listed client IDs proceed normally
  • Requests from unlisted client IDs receive 403 Forbidden
  • The health check endpoint (/) remains accessible

Security Notes

  • Data is encrypted: Even without access control, your task data is end-to-end encrypted. The server only sees opaque blobs.
  • Client ID as shared secret: Your client ID acts as a weak form of authentication. Keep it private.
  • HTTPS required: Always deploy behind HTTPS (Cloudflare Workers does this automatically).

Development

# Install dependencies
npm install
# Run locally
npm run dev
# Apply database migrations (local)
wrangler d1 migrations apply taskchampion-sync --local
# Run tests
npm test# Lint
npm run lint

Related Projects

License

MIT

About

Taskwarrior server implementation on Cloudflare Workers

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - ndisidore/taskworker: Taskwarrior server implementation on Cloudflare Workers · GitHub
Skip to content

Repository files navigation

TaskWorker

A TaskChampion sync server implementation built on Cloudflare Workers and D1.

What is This?

This is a sync server for Taskwarrior 3.0+, implementing the TaskChampion sync protocol. It allows you to synchronize tasks between multiple devices.

Why?

As a general TUI fanboy, I've come to love Taskwarrior. But one of its main pitfalls is hosting the server, so much so that many derivatives have spun up simply because they make the hosting story easier. As a Cloudflare-ain (who has worked on Workers) I knew what needed to be done :)

Architecture

Understanding how TaskChampion sync works is key to understanding this project.

The Two Pieces

┌────────────────────────────────────────────────────────────────┐
│ YOUR DEVICES │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Laptop │ │ Desktop │ │ Phone │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ Task │ │ │ │ Task │ │ │ │ Task │ │ │
│ │ │Warrior│ │ │ │Warrior│ │ │ │Warrior│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │ ┌───┴───┐ │ │
│ │ │ TC │ │ │ │ TC │ │ │ │ TC │ │ │
│ │ │Library│ │ │ │Library│ │ │ │Library│ │ │
│ │ └───┬───┘ │ │ └───┬───┘ │ │ └───┬───┘ │ │
│ └──────┼──────┘ └──────┼──────┘ └──────┼──────┘ │
│ │ encrypted │ encrypted │ encrypted │
└─────────┼──────────────────┼──────────────────┼────────────────┘
│ │ │
└──────────────────┼──────────────────┘
│
▼
┌───────────────────┐
│ TaskWorker │
│ (this project) │
│ │
│ Stores encrypted │
│ blobs only - │
│ cannot read your │
│ task data │
└─────────┬─────────┘
│
▼
┌───────────────────┐
│ Cloudflare D1 │
│ (SQLite) │
└───────────────────┘

TaskChampion Library (client-side, in Taskwarrior):

  • Manages your local task database
  • Understands tasks, tags, annotations, dependencies
  • Encrypts all data before sending to the server
  • Decrypts data received from the server

TaskWorker / Sync Server (this project):

  • Stores encrypted binary blobs ("history segments" and "snapshots")
  • Tracks version chains for sync ordering
  • Cannot read your task data - it's encrypted end-to-end
  • Acts as a relay between your devices

Why the Server is "Dumb"

This is a security feature. The server never has access to the encryption key, so even if compromised, your task data remains private. The server only sees opaque binary blobs.

This is why our implementation doesn't need Task, Tag, or Annotation types - we literally cannot read them (intentional per the TaskChampion sync protocol).

Protocol

The sync protocol uses 4 HTTP endpoints:

EndpointMethodPurpose
/v1/client/add-version/:parentVersionIdPOSTSubmit encrypted changes
/v1/client/get-child-version/:parentVersionIdGETFetch next version
/v1/client/add-snapshot/:versionIdPOSTStore encrypted snapshot
/v1/client/snapshotGETFetch latest snapshot

Clients identify themselves via the X-Client-Id header (a UUID shared across your devices).

Deployment

# Install dependencies
npm install
# Create the D1 database
wrangler d1 create taskchampion-sync
# Apply database migrations
wrangler d1 migrations apply taskchampion-sync --remote
# Deploy to Cloudflare Workers
npm run deploy

Your server will be available at https://taskworker.<your-subdomain>.workers.dev.

Usage with Taskwarrior

Prerequisites

  • Taskwarrior 3.0+ (uses TaskChampion sync protocol)
  • A deployed TaskWorker instance (see Deployment above)

Configuration

  1. Generate a client ID (UUID shared across all your devices):

    uuidgen
    # Example output: a1b2c3d4-e5f6-7890-abcd-ef1234567890
  2. Generate an encryption secret (keeps your data private):

    openssl rand -hex 32
    # Example output: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
  3. Configure Taskwarrior by adding to ~/.taskrc:

    # Sync server URL (replace with your deployed URL)sync.server.url=https://taskworker.your-subdomain.workers.dev
    # Client ID (same UUID on all your devices)sync.server.client_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890
    # Encryption secret (same secret on all your devices - keep this private!)sync.encryption_secret=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

    Important: Use the same client_id and encryption_secret on all devices you want to sync.

Syncing

# Sync your tasks
task sync
# First sync on a new device
task sync init

Example Workflow

# On your laptop
task add "Write README" project:docs
task sync
# On your desktop (after initial setup with same credentials)
task sync
task list
# You'll see "Write README" synced from your laptop

Access Control

By default, TaskWorker runs in open mode - any client with a valid UUID can sync. For personal/production use, you should restrict access to only your devices.

Client ID Allowlist

Set the ALLOWED_CLIENT_IDS environment variable to restrict which clients can access your server:

# Using wrangler secret (recommended for production)
wrangler secret put ALLOWED_CLIENT_IDS
# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890# Or for multiple clients (comma-separated)# Enter: a1b2c3d4-e5f6-7890-abcd-ef1234567890,11111111-2222-3333-4444-555555555555

Alternatively, set it in wrangler.jsonc for development:

{
"vars": {
"ALLOWED_CLIENT_IDS": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}

When the allowlist is configured:

  • Requests from listed client IDs proceed normally
  • Requests from unlisted client IDs receive 403 Forbidden
  • The health check endpoint (/) remains accessible

Security Notes

  • Data is encrypted: Even without access control, your task data is end-to-end encrypted. The server only sees opaque blobs.
  • Client ID as shared secret: Your client ID acts as a weak form of authentication. Keep it private.
  • HTTPS required: Always deploy behind HTTPS (Cloudflare Workers does this automatically).

Development

# Install dependencies
npm install
# Run locally
npm run dev
# Apply database migrations (local)
wrangler d1 migrations apply taskchampion-sync --local
# Run tests
npm test# Lint
npm run lint

Related Projects

License

MIT

About

Taskwarrior server implementation on Cloudflare Workers

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages