Skip to content

Feature Flags ​

TT Time Tracker supports per-tenant feature toggles. Some features can be disabled for specific workspaces — for example, a tenant that only tracks time and never uploads invoices can turn off the invoice feature entirely.

Where flags are stored ​

Feature settings are stored as a JSON column on the Tenant model:

prisma
model Tenant {
  features Json?  // FeatureSettings
}

The FeatureSettings shape:

typescript
type FeatureSettings = {
  projects?: boolean         // default: true
  vehicles?: boolean         // default: true
  zones?: boolean            // requires projects; default: false
  tasks?: boolean            // requires projects; default: false
  invoices?: boolean         // requires projects; default: false
  hoursOverview?: boolean    // default: true
  emailSync?: boolean        // default: true
  punchClock?: boolean       // default: false
  taskTimer?: boolean        // default: false
  approvalWorkflow?: boolean // default: true
}

A missing field means the default applies.

Frontend: useTenant() store ​

The tenant store exposes computed properties for each flag:

typescript
const tenant = useTenant()

tenant.hasProjects        // projects feature
tenant.hasVehicles        // vehicles feature
tenant.hasZones           // zones (requires hasProjects)
tenant.hasTasks           // tasks (requires hasProjects)
tenant.hasInvoices        // invoices (requires hasProjects)
tenant.hasHoursOverview   // hours overview dashboard
tenant.hasEmailSync       // email sync configuration
tenant.hasPunchClock      // punch clock widget
tenant.hasTaskTimer       // allocation timer (week view)
tenant.hasApprovalWorkflow // entry approval workflow

These are used in components to conditionally render UI and in route guards to redirect users away from disabled features.

Backend: FeatureGuard ​

The FeatureGuard and @RequireFeature() decorator gate API endpoints:

typescript
import { RequireFeature } from '../../casl/feature-flag.decorator'

@Get()
@RequireFeature('emailSync')
findSyncs() { ... }

If the tenant's features.emailSync is false, the guard returns 403 Forbidden before the controller method runs.

Enabling/disabling features ​

Admins can toggle features in the workspace Settings page. Feature changes take effect immediately — the frontend re-fetches the tenant on each navigation.

Super admins can also modify features directly via the super-admin tenant management UI at /admin/tenants/:tenantId.

Dependency between flags ​

Some flags only make sense when a parent flag is enabled:

  • zones requires projects: true
  • tasks requires projects: true
  • invoices requires projects: true

The frontend store enforces these dependencies:

typescript
const hasZones = computed(() => !!hasProjects.value && !!features.value?.zones)
const hasTasks = computed(() => !!hasProjects.value && !!features.value?.tasks)
const hasInvoices = computed(() => !!hasProjects.value && !!features.value?.invoices)

If projects is disabled, zones, tasks, and invoices are also effectively disabled regardless of their individual settings.

The task rule ​

With tasks effectively on, an entry on a project whose task list offers at least one active task must carry one of that list's tasks. It is one rule in two places that must agree:

  • Client — projectRequiresTask / taskListRequiresTask (services/client/src/utils/taskRequirement.ts). Every entry form reads it through useEntry (taskRequired shows the task field; completed gates saving), and the week grid and the task timer call it directly. Resolve the project's task list from the projects collection (useTasks) — an entry row's project is trimmed to { id, name }.
  • API — assertTaskMatchesProject (services/api/src/common/org-relations.util.ts), applied by EntryService.create/update and TaskTimerService.switchTo. It answers 400 ENTRY_TASK_REQUIRED or 400 ENTRY_TASK_NOT_IN_PROJECT (a task off the project's list). The controllers decide whether it applies with tasksEnabled(features) — projects && tasks, like hasTasks — from the flags they already read.

An update that leaves the project and the task as they were is exempt on both sides, so entries logged before the rule stay editable. A timer slice is checked when it starts, never when it closes.

TT Time Tracker — Internal Documentation