A template repository for creating plugins for the TimeTracker application.
This template provides a complete foundation for building TimeTracker plugins, including:
- Rust Backend: Dynamic library that integrates with TimeTracker's plugin system
- React Frontend: UI components that can be embedded in TimeTracker's interface
- Build System: GitHub Actions workflow for automated cross-platform builds
- Plugin Manifest: TOML configuration file defining plugin metadata
- Extension API: Extend Core entities (activities, manual_entries, categories) with custom fields
Click "Use this template" on GitHub to create your own plugin repository, or:
git clone https://github.com/tmtrckr/plugin-template.git my-plugin
cd my-plugin
rm -rf .git
git initEdit plugin.toml with your plugin information:
[plugin]
name = "my-plugin" # Unique plugin ID (must match registry entry)
display_name = "My Plugin" # Display name
version = "1.0.0"
author = "Your Name" # Required: Plugin author (must match normalized author name in registry)
description = "Description of your plugin"
repository = "https://github.com/your-username/my-plugin"
license = "MIT"
api_version = "1.0"
min_core_version = "0.3.0" # Minimum required TimeTracker version
max_core_version = "1.0.0" # Maximum supported TimeTracker versionUpdate the [package] section in Cargo.toml:
[package]
name = "my-plugin"
version = "1.0.0"
authors = ["Your Name <your.email@example.com>"]
description = "Description of your plugin"
[lib]
name = "my_plugin_backend"
crate-type = ["cdylib"] # Dynamic library for plugin loading
[dependencies]
time-tracker-plugin-sdk = "0.2.10" # Use the published SDK from crates.io
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"Important: The SDK dependency is already included in the template. Make sure to use the same version that matches your target TimeTracker version. The SDK is published on crates.io.
For development alongside the main app, you can use a local path:
time-tracker-plugin-sdk = { path = "../time-tracker-app/plugin-sdk" }The backend section specifies the compiled library filename:
[backend]
library_name = "my_plugin_backend" # Library filename (without extension, e.g., "my_plugin_backend.dll" on Windows)Note:
- The
library_nameshould match your actual compiled library filename - The entry point function is always
_plugin_create(hardcoded in the loader) - The loader will search for library files if
library_namedoesn't match exactly
Edit src/plugin.rs to implement your plugin logic. The plugin must implement the Plugin trait from time-tracker-plugin-sdk:
use time_tracker_plugin_sdk::{Plugin, PluginInfo, PluginAPIInterface};
pub struct MyPlugin {
info: PluginInfo,
}
impl Plugin for MyPlugin {
fn info(&self) -> &PluginInfo {
&self.info
}
fn initialize(&mut self, api: &dyn PluginAPIInterface) -> Result<(), String> {
// Register schema extensions, model extensions, etc.
Ok(())
}
fn invoke_command(&self, command: &str, params: serde_json::Value, api: &dyn PluginAPIInterface) -> Result<serde_json::Value, String> {
// Handle plugin commands
Ok(serde_json::json!({}))
}
fn shutdown(&self) -> Result<(), String> {
// Clean up resources
Ok(())
}
fn get_schema_extensions(&self) -> Vec<SchemaExtension> {
vec![]
}
fn get_frontend_bundle(&self) -> Option<Vec<u8>> {
None
}
}In src/lib.rs, export the required FFI functions:
use time_tracker_plugin_sdk::Plugin;
#[no_mangle]
pub extern "C" fn _plugin_create() -> *mut dyn Plugin {
Box::into_raw(Box::new(plugin::MyPlugin::new()))
}
#[no_mangle]
pub extern "C" fn _plugin_destroy(plugin: *mut dyn Plugin) {
unsafe {
let _ = Box::from_raw(plugin);
}
}# Build Rust backend
cargo build --release
# Build frontend (if you have frontend components)
cd frontend
npm install
npm run buildCreate a GitHub Release to trigger automatic builds for all platforms:
- Create a new release tag (e.g.,
v1.0.0) - GitHub Actions will automatically build for Windows, macOS, and Linux
- Download the build artifacts from the release page
time-tracker-plugin-template/
├── .github/
│ └── workflows/
│ └── build.yml # CI/CD workflow for building plugins
├── src/
│ ├── lib.rs # FFI exports (_plugin_create, _plugin_destroy)
│ └── plugin.rs # Your plugin implementation
├── frontend/
│ ├── src/
│ │ └── index.tsx # Plugin entry (exports initialize/cleanup)
│ ├── index.js # Built bundle (output of npm run build)
│ ├── package.json
│ ├── vite.config.ts
│ └── tsconfig.json
├── migrations/
│ └── 001_initial.sql # Database migrations (optional)
├── plugin.toml # Plugin manifest
├── Cargo.toml # Rust dependencies
├── .gitignore
└── README.md
The plugin.toml file defines your plugin's metadata and configuration:
name: Unique identifier for your plugin (used in URLs and internal references, must match registry entryid)display_name: Human-readable name shown in the UI (maps to registrynamefield)version: Semantic version (e.g., "1.0.0", maps to registrylatest_version)author: Plugin author name (required, must match normalized author name in registry)description: Brief description of what your plugin doesrepository: GitHub repository URL (must be a valid GitHub repository)license: License identifier (MIT, Apache-2.0, etc., SPDX format)api_version: Plugin API version your plugin targets (currently "1.0")min_core_version: Minimum TimeTracker version required (e.g., "0.3.0")max_core_version: Maximum supported TimeTracker version (e.g., "1.0.0")
library_name: Name of the compiled library file (used to locate the library, e.g.,"my_plugin_backend"formy_plugin_backend.dllon Windows)- The loader will search for library files if this doesn't match exactly
- Entry point function is always
_plugin_create(hardcoded in the loader)
entry: Path to compiled JavaScript bundle (e.g.frontend/index.js)components: List of React component names the plugin registers (for reference)
targets: List of Rust target triples to build for
All plugins must implement the Plugin trait from time-tracker-plugin-sdk:
pub trait Plugin: Send + Sync {
/// Get plugin metadata
fn info(&self) -> &PluginInfo;
/// Initialize the plugin
fn initialize(&mut self, api: &dyn PluginAPIInterface) -> Result<(), String>;
/// Invoke a command on the plugin
fn invoke_command(&self, command: &str, params: serde_json::Value, api: &dyn PluginAPIInterface) -> Result<serde_json::Value, String>;
/// Shutdown the plugin
fn shutdown(&self) -> Result<(), String>;
/// Get schema extensions that this plugin requires
fn get_schema_extensions(&self) -> Vec<SchemaExtension>;
/// Get frontend bundle bytes (if plugin provides UI)
fn get_frontend_bundle(&self) -> Option<Vec<u8>>;
}The PluginAPIInterface provides access to TimeTracker functionality:
pub trait PluginAPIInterface: Send + Sync {
/// Register a database schema extension
fn register_schema_extension(
&self,
entity_type: EntityType,
schema_changes: Vec<SchemaChange>,
) -> Result<(), String>;
/// Register a model extension
fn register_model_extension(
&self,
entity_type: EntityType,
model_fields: Vec<ModelField>,
) -> Result<(), String>;
/// Register query filters
fn register_query_filters(
&self,
entity_type: EntityType,
query_filters: Vec<QueryFilter>,
) -> Result<(), String>;
/// Call a database method by name with JSON parameters
fn call_db_method(&self, method: &str, params: serde_json::Value) -> Result<serde_json::Value, String>;
}Plugins can extend Core entities (activities, manual_entries, categories) using the Extension API:
Add columns to Core tables or create new tables:
api.register_schema_extension(
EntityType::Activity,
vec![
// Create a new table
SchemaChange::CreateTable {
table: "my_plugin_data".to_string(),
columns: vec![
TableColumn {
name: "id".to_string(),
column_type: "INTEGER".to_string(),
primary_key: true,
nullable: false,
default: None,
foreign_key: None,
},
// ... more columns
],
},
// Add a column to activities table
SchemaChange::AddColumn {
table: "activities".to_string(),
column: "custom_field".to_string(),
column_type: "TEXT".to_string(),
default: None,
foreign_key: None,
},
// Add an index
SchemaChange::AddIndex {
table: "activities".to_string(),
index: "idx_activities_custom_field".to_string(),
columns: vec!["custom_field".to_string()],
},
],
)?;Add fields to Core data structures:
api.register_model_extension(
EntityType::Activity,
vec![
ModelField {
name: "custom_field".to_string(),
type_: "Option<String>".to_string(),
optional: true,
},
],
)?;Add custom query filters for activities:
api.register_query_filters(
EntityType::Activity,
vec![
QueryFilter {
name: "by_custom_field".to_string(),
filter_fn: Box::new(|activities, params| {
// Filter logic
Ok(activities)
}),
},
],
)?;initialize: Called when the plugin is first loaded. Use this to register extensions and set up your plugin.invoke_command: Called when a command is invoked on your plugin. Handle your plugin's commands here.shutdown: Called when the plugin is unloaded. Clean up any resources here.
The frontend must be built as a single bundle (frontend/index.js) with no relative imports to plugin source files in the built output. Use the template's vite.config.ts as a reference.
- Allowed external dependencies (resolved at runtime by the app):
react,react/jsx-runtime,react-dom,lucide-react,date-fns, and other npm packages fromnode_modules. - App-provided modules (must be marked external in Vite):
./store(useStore),./utils/format(formatTimerTime),./utils/toast(showSuccess,showError,handleApiError),./components/Common/Button,./components/Common/Card. Import them as in the app (e.g.import { useStore } from "./store";). - All other plugin code must be bundled into
frontend/index.js(no./hooks/...or./components/...imports that point at unbundled plugin files).
See the TimeTracker Plugin Developer Guide (in the main app repo or docs) for the full Vite external list, checklist after build, and troubleshooting.
Export a default object with initialize(api) (and optionally cleanup()) from frontend/src/index.tsx. Register routes, sidebar items, dashboard widgets, and settings tabs inside initialize using the API provided by TimeTracker:
import React from 'react';
const MySettings: React.FC = () => {
return <div>Plugin Settings UI</div>;
};
export default {
initialize(api) {
if (typeof api.registerSettingsTab === 'function') {
api.registerSettingsTab('MySettings', MySettings);
}
},
cleanup() {
// Clean up when the plugin is unloaded
},
};List component names in plugin.toml for reference:
[frontend]
entry = "frontend/index.js"
components = ["MySettings"]Plugins can interact with the database through the call_db_method API:
fn invoke_command(&self, command: &str, params: serde_json::Value, api: &dyn PluginAPIInterface) -> Result<serde_json::Value, String> {
match command {
"get_my_data" => {
// Call a database method
api.call_db_method("get_my_data", params)
}
"create_my_data" => {
api.call_db_method("create_my_data", params)
}
_ => Err(format!("Unknown command: {}", command)),
}
}Note: Database methods must be implemented in the core app's database.rs. For custom tables created by your plugin, you'll need to add corresponding methods to the core app or use raw SQL through the API.
Build for your current platform:
cargo build --release
cd frontend && npm run buildUse GitHub Actions (recommended) or build locally with cross-compilation:
# Install cross-compilation tools
rustup target add x86_64-pc-windows-msvc
rustup target add x86_64-apple-darwin
rustup target add x86_64-unknown-linux-gnu
# Build for each platform
cargo build --release --target x86_64-pc-windows-msvc
cargo build --release --target x86_64-apple-darwin
cargo build --release --target x86_64-unknown-linux-gnu- Update version in
plugin.tomlandCargo.toml - Commit changes
- Create a git tag:
git tag v1.0.0 - Push tag:
git push origin v1.0.0 - Create a GitHub Release with the same tag
- GitHub Actions will automatically build and attach artifacts
To make your plugin discoverable in the Time Tracker Marketplace:
-
Ensure your plugin meets requirements:
- Plugin must be hosted on GitHub
- Repository must have a
plugin.tomlmanifest file - Plugin must have at least one GitHub Release with compiled binaries
-
Add your plugin to the registry:
- Fork the Plugins Registry repository
- Use the interactive script:
npm run create-plugin(recommended) - Or manually create a
plugin.jsonentry following the registry structure - Submit a pull request to the registry
-
Registry fields mapping:
plugin.tomlname→ registryidplugin.tomldisplay_name→ registrynameplugin.tomlversion→ registrylatest_versionplugin.tomlauthor→ registryauthor(required, must match normalized author directory name)plugin.tomlmin_core_version→ registrymin_core_versionplugin.tomlmax_core_version→ registrymax_core_version
See the Plugins Registry README for detailed instructions.
- Build your plugin (see Building section)
- Copy the built library to TimeTracker's plugins directory:
- Windows:
%APPDATA%\timetracker\plugins\your-plugin-name\ - macOS:
~/Library/Application Support/timetracker/plugins/your-plugin-name/ - Linux:
~/.local/share/timetracker/plugins/your-plugin-name/
- Windows:
- Copy
plugin.tomland frontend build (if any) to the same directory - Restart TimeTracker
- Enable your plugin in Settings → Plugins
Enable debug logging in TimeTracker to see plugin logs:
# Windows
set RUST_LOG=debug
time-tracker-app.exe
# macOS/Linux
RUST_LOG=debug ./time-tracker-appfn initialize(&mut self, api: &dyn PluginAPIInterface) -> Result<(), String> {
// Add a custom field to activities
api.register_schema_extension(
EntityType::Activity,
vec![
SchemaChange::AddColumn {
table: "activities".to_string(),
column: "priority".to_string(),
column_type: "INTEGER".to_string(),
default: Some("0".to_string()),
foreign_key: None,
},
],
)?;
api.register_model_extension(
EntityType::Activity,
vec![
ModelField {
name: "priority".to_string(),
type_: "Option<i32>".to_string(),
optional: true,
},
],
)?;
Ok(())
}fn initialize(&mut self, api: &dyn PluginAPIInterface) -> Result<(), String> {
api.register_schema_extension(
EntityType::Activity,
vec![
SchemaChange::CreateTable {
table: "notes".to_string(),
columns: vec![
TableColumn {
name: "id".to_string(),
column_type: "INTEGER".to_string(),
primary_key: true,
nullable: false,
default: None,
foreign_key: None,
},
TableColumn {
name: "activity_id".to_string(),
column_type: "INTEGER".to_string(),
primary_key: false,
nullable: false,
default: None,
foreign_key: Some(ForeignKey {
table: "activities".to_string(),
column: "id".to_string(),
}),
},
TableColumn {
name: "content".to_string(),
column_type: "TEXT".to_string(),
primary_key: false,
nullable: false,
default: None,
foreign_key: None,
},
],
},
],
)?;
Ok(())
}export const MyPluginSettings: React.FC = () => {
const [enabled, setEnabled] = React.useState(false);
return (
<div className="p-4">
<h2>My Plugin Settings</h2>
<label>
<input
type="checkbox"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
/>
Enable feature
</label>
</div>
);
};- Use Semantic Versioning
- Increment major version for breaking API changes
- Increment minor version for new features
- Increment patch version for bug fixes
Always return proper errors from trait methods:
fn initialize(&mut self, api: &dyn PluginAPIInterface) -> Result<(), String> {
api.register_schema_extension(...)
.map_err(|e| format!("Failed to register schema: {}", e))?;
Ok(())
}- Clean up resources in
shutdown - Don't hold references to API after shutdown
- Use proper error handling for all operations
- Validate all user input
- Don't expose sensitive data in frontend components
- Use parameterized queries for database operations (handled by core app)
- Always register schema extensions before model extensions
- Use foreign keys for referential integrity
- Add indexes for frequently queried columns
- Keep extension fields optional when possible for compatibility
When contributing to this template:
- Keep it simple and focused
- Provide clear examples
- Document all public APIs
- Test on all platforms
This template is licensed under the MIT License. See LICENSE file for details.
- TimeTracker Repository
- TimeTracker Plugin Developer Guide (frontend bundle requirements, external modules, Vite config) — see the main app docs or repo
- Rust Plugin Development Guide
- React Documentation
- Tauri Documentation
For issues and questions:
- Open an issue in this repository for template-related questions
- Open an issue in TimeTracker for plugin API questions