Workspace Settings
TruePPM’s workspace is the installation itself — a single-tenant configuration row that backs the pages under Workspace → Settings: General, Members, Invites, Groups & teams, and Programs, plus workspace archive/delete actions (#517/#518/#519, ADR-0087). ADR-0087 scoped the original three — General, Members, and Groups & teams — and the section has grown since.
Multi-tenancy is an Enterprise feature. In the community edition there is exactly one workspace per deployment.
In-product help
Section titled “In-product help”Settings help comes at two altitudes: a link on every section, and a ⓘ on individual fields that need it.
Section help: “Learn more →”
Section titled “Section help: “Learn more →””Every section in Workspace, Program, and Project settings ends its description with a Learn more → link to the page documenting that section. It sits at the tail of the sentence under the section title, opens in a new tab, and — like the field-level ⓘ below — is not permission-gated: someone who cannot change a setting can still read what it does.
The link is announced to screen readers by section (“Learn more about General, opens in a new tab”), not as a bare “Learn more”, so the several dozen of them stay distinguishable when tabbing or listing links.
Section help answers “what is this page and where is it documented”. Field help, below, answers “what are my choices for this input”. They are complementary, and most sections carry both.
Field help: the ⓘ popover
Section titled “Field help: the ⓘ popover”Settings fields that carry jargon, a policy choice, or an inheritance cascade also carry a contextual-help affordance — a circled ⓘ in the field’s label row. Opening it explains the setting in plain language and, where a deeper guide exists, deep-links to it via Learn more →.
The ⓘ supplements the always-visible hint beneath each field rather than replacing it. The hint answers “what does my current pick mean”; the popover answers “what are all my choices”.
Three behaviors are worth knowing:
- It is a non-modal dialog, not a tooltip. The panel contains a link, and a link
inside an
aria-describedbytooltip cannot be reached, so the affordance is arole="dialog"witharia-modal="false". Focus moves into the panel on open, Tab reaches the Learn more → link, and Esc closes the panel and returns focus to the ⓘ. Opened from inside a settings modal, Esc peels one layer at a time — the popover first, the modal second. - There is no permission gate. The help renders regardless of role, so a member with read-only access can understand a setting without the rights to change it.
- Its screen-reader name is the field. The trigger announces as “About the <field> options” rather than as an unlabeled icon button.
On the General page the ⓘ appears on these fields:
| Field | Learn more → |
|---|---|
| Default timezone | Timezone & date format |
| Fiscal year starts | Fiscal year start |
| Work week | Working calendars |
| Default project view | This page |
| Iteration terminology | Methodology preset |
| Allow guests | Sharing & access |
| Public sharing | Sharing & access |
| Keep Monte Carlo run history | Forecast history |
| Run history limit | Forecast history |
| Run attribution visible to | Forecast history |
| Story picker shows Ready stories only, by default | Sprint backlog table |
| Duration change → percent complete | This page |
| Program & project overrides | This page |
| Forecast-history overrides | Retention |
The same affordance appears throughout the program and project settings pages.
General (/settings/general)
Section titled “General (/settings/general)”Fields
Section titled “Fields”| Field | Type | Default | Description |
|---|---|---|---|
name | string | "TruePPM Workspace" | Display name shown in the nav header and email footers. |
subdomain | string | "" | Read-only via the API. Reserved for a future hosted edition; self-hosted installs leave this blank. |
timezone | string (IANA) | "UTC" | Default timezone used for display and for interpreting dates without explicit timezone info. |
fiscal_year_start_month | integer (1–12) | 1 | Fiscal-year start month. Drives quarter labels across the workspace, including the Schedule timeline. |
fiscal_year_start_day | integer (1–31) | 1 | Fiscal-year start day, validated against the month (year-agnostic: February caps at 28; 30-day months reject 31). |
fiscal_year_start_display | string | "January 1" | Read-only. Human label derived from month + day, e.g. "April 6". |
work_week | array of 7 booleans | Mon–Fri true, Sat–Sun false | Working-day flags, Monday through Sunday. Controls which days the CPM engine treats as working days when no project calendar overrides. |
default_project_view | string | "board" | The view tab that opens by default when a user opens a project ("board", "schedule", etc.). |
allow_guests | boolean | true | Whether users with guest status may be added to projects. This is the workspace default; programs and projects inherit it and may override it per scope. See Sharing & Access Inheritance. |
public_sharing | boolean | false | When true, designated read-only views may be shared via link so anyone with the link can view without signing in. This is the workspace default; programs and projects inherit it and may override it per scope. See Sharing & Access Inheritance. |
public_sharing_override_policy | string | "suggest" | Whether downstream scopes may override the workspace sharing values. "suggest" (default) lets programs/projects override; "enforce" makes the workspace value a hard ceiling. enforce is an Enterprise capability — in the community edition it degrades to suggest (no lock). |
sprint_picker_ready_only_default (ships in 0.4) | boolean | true | Whether the sprint story picker starts filtered to Definition-of-Ready stories. This is the workspace default; programs and projects inherit it and may override it per scope (Shape A: null override = inherit). Advisory only — the picker’s own “Show all” toggle always reveals a not-ready story, and committing one is never blocked. There is no override policy / enforcement seam for this field. |
Access
Section titled “Access”- Any active workspace member can
GET /api/v1/workspace/. - Workspace Admin or Owner is required to
PATCH /api/v1/workspace/.
The workspace row is created lazily on first access — no seed migration is needed on a fresh installation.
Fiscal year start
Section titled “Fiscal year start”The Fiscal year starts control offers four quick presets (Jan 1, Apr 1,
Jul 1, Oct 1) plus a Custom… option that opens a month + day picker for
arbitrary starts such as the UK tax year (April 6). The value is year-agnostic —
it stores only month and day — so the day is validated against the month
(February is capped at 28; 30-day months reject 31), enforced server-side on
PATCH.
This anchor controls how quarters are labeled across the workspace. On the
Schedule timeline a fiscal year that starts in April shows Q1 = Apr–Jun, labeled
Q1 FY27 (fiscal years are named by the calendar year in which they end). See
Fiscal quarters.
Upgrade note. This setting replaced the earlier free-text
fiscal_year_startstring. The upgrade migration parses existing values ("January 1","April","4/1", …) into the structured month/day pair; anything unrecognized falls back to January 1 and is logged.
Workspace logo
Section titled “Workspace logo”The Workspace logo control lets an Owner or Admin upload a square logo that surfaces in the top bar beside the workspace name. When no logo is set, the top bar falls back to a letter-mark derived from the workspace name.
- Formats: PNG or WebP only. SVG is rejected — an SVG can carry embedded script, so accepting one would open a stored-XSS vector.
- Size: 2 MB maximum. Larger files return
HTTP 413. - Dimensions: at least 256×256 is recommended. The browser warns below that size but still allows the upload; the server does not enforce a minimum.
- Validation: the server identifies the image by its magic bytes, not the
declared
Content-Type, so a mislabeled or disguised file is rejected withHTTP 415.
The logo is served from a public endpoint (GET /api/v1/workspace/logo/)
with X-Content-Type-Options: nosniff and Content-Disposition: inline — branding
is non-sensitive, and a public URL keeps it usable in an <img> tag without
attaching a bearer token. Replacing the logo deletes the previous blob; Remove
(DELETE /api/v1/workspace/logo/) clears it and restores the letter-mark.
| Method | Path | Access | Description |
|---|---|---|---|
GET | /api/v1/workspace/logo/ | Public | Serve the current logo (404 when unset). |
POST | /api/v1/workspace/logo/ | Admin+ | Upload/replace the logo (multipart file). |
DELETE | /api/v1/workspace/logo/ | Admin+ | Clear the logo. |
The General settings response exposes logo_url (a cache-busting public URL) or
null when no logo is set.
Members (/settings/members)
Section titled “Members (/settings/members)”Workspace role model
Section titled “Workspace role model”Workspace roles are separate from per-project roles and use a coarser three-level hierarchy:
| Role | Ordinal | Description |
|---|---|---|
| Member | 100 | Default for all workspace users. Can read workspace-level data and access projects they are invited to. |
| Admin | 300 | Can manage members (invite, change roles, deactivate), manage groups, and edit workspace-level settings. |
| Owner | 400 | Same capabilities as Admin. At least one Owner must exist at all times (last-Owner guard). |
These role ordinals are distinct from the five project-scoped roles (Owner/Admin/Scheduler/Member/Viewer — see Roles and Permissions). A workspace Member may hold any project role; a workspace Admin is not automatically an admin on any project.
Member status
Section titled “Member status”status is orthogonal to role — it tracks account lifecycle, not permission tier:
| Status | Meaning |
|---|---|
active | Normal — the user can authenticate and access their projects. |
guest | External collaborator. Permitted only when allow_guests is enabled on the workspace. |
deactivated | The user’s Django account is disabled (is_active=false) and they cannot authenticate. Deactivation does not delete the user or their data. |
Deactivating a user sets auth.User.is_active = false atomically inside the same
database transaction — the user is immediately locked out of authentication. To
restore access, set their status back to active.
Last-Owner guard
Section titled “Last-Owner guard”The workspace must always have at least one user with the Owner role. Attempting
to demote, deactivate, or remove the last Owner returns HTTP 400:
{"detail": "Cannot demote the last Owner of the workspace."}Display-only fields (sso, two_fa)
Section titled “Display-only fields (sso, two_fa)”Member list responses include sso: false and two_fa: false in the community
edition. These fields report governed SSO and two-factor enforcement —
whether a member is provisioned and policy-controlled from a directory of record
— which is an Enterprise feature; the fields are placeholders and carry no
functional meaning in OSS.
They do not describe basic login federation. Pointing TruePPM at your own identity provider so your team logs in via OIDC / OAuth2 ships in the OSS core at 0.4 — that is login-only federation, not directory governance. For the full carve-out (log in via your own IdP → OSS; provision/deprovision/govern from a directory → Enterprise) and a dated comparison against the open-core competition, see SSO Is Not an Enterprise Feature.
Export members as CSV
Section titled “Export members as CSV”The Members page provides an Export CSV action that downloads the member list as a CSV file. The export is generated entirely in the browser — it requires no server endpoint and never leaves the client until you save it.
- The file is named
trueppm-workspace-members.csv. - Columns are Name, Email, Role, Status, and Groups (a member’s groups are joined into one semicolon-separated cell).
- The export reflects the currently visible rows — if a search term or role filter is active, only the matching members are exported. Clear the filters to export the full roster.
This feature was added in 0.3.
Access
Section titled “Access”- Workspace Admin+ can list all members and perform role/status changes.
- Non-admin members see only their own membership row.
- A user cannot assign a role above their own (
HTTP 403if attempted).
Invites (/settings/members → invite flow)
Section titled “Invites (/settings/members → invite flow)”Workspace Admins send email invitations to bring new users into the workspace.
How invites work
Section titled “How invites work”- An Admin
POSTs to/api/v1/workspace/invites/with{email, role}. - The API creates a pending invite row and sets
email_pending=true. The raw token is emailed to the recipient, never stored in the database (only its SHA-256 hash is persisted). - The
drain_invite_emailsCelery Beat task dispatches the email every 30 s (5-minute orphan window to avoid racing the create transaction). Email delivery failures are retried up to 3 times; at exhaustion the invite is markedfailed, and an admin can re-send it (see Resend an invite) without revoking and re-creating it. - The recipient clicks the link to reach the accept flow. They
POSTto/api/v1/workspace/invites/accept/with{token, username, password}. This endpoint:- is publicly accessible (no session required),
- hashes the submitted token and looks up a non-expired pending invite,
- provisions a new
Useraccount or links the invite to an existing account if the invite email matches, - creates a
WorkspaceMembershipat the invited role, - marks the invite
accepted.
- Error responses are generic (“invalid or expired token”) to prevent token enumeration.
Token security
Section titled “Token security”- Tokens are generated with
secrets.token_urlsafe(32)(256 bits of entropy). - Only the SHA-256 hash is stored permanently.
- The raw token is held transiently in
email_tokenuntil the drain sends the email, then cleared — a database snapshot taken after delivery contains only the hash. - The accept endpoint is rate-limited to 20 requests/minute per IP address.
Invite TTL and statuses
Section titled “Invite TTL and statuses”Invites expire 7 days after creation. Statuses:
| Status | Meaning |
|---|---|
pending | Awaiting acceptance (or email delivery). |
accepted | Accepted; membership created. |
revoked | Canceled by an Admin before acceptance. |
expired | TTL elapsed without acceptance. |
Accepted, revoked, and expired invites older than 30 days are purged by a nightly
purge_stale_invites Beat task.
Resend an invite
Section titled “Resend an invite”A pending or failed invite can be re-sent without revoking and re-creating
it. The Members page offers a per-row Resend action and a Resend all
button that re-queues every outstanding invite in one request. This was added in 0.3.
Resending re-issues the token: a fresh raw token is generated and emailed, so any earlier link the recipient still holds stops working. The invite’s 7-day TTL is reset from the resend, and the email re-enters the same outbox drain described above. A resend on an invite whose email is still in flight is an idempotent no-op — it will not send twice.
| Method | Path | Access | Description |
|---|---|---|---|
POST | /api/v1/workspace/invites/{id}/resend/ | Admin+ | Re-issue and re-queue one invite. Returns 202 {"queued": true}. |
POST | /api/v1/workspace/invites/resend-all/ | Admin+ | Re-queue every pending/failed invite. Returns 202 {"requeued": <count>}. |
Only pending and failed invites are resendable — resending an accepted,
revoked, or expired invite returns HTTP 409. The per-invite endpoint is
rate-limited to 5 requests/minute; the bulk endpoint bundles every invite
into a single throttle bucket so it cannot be used to flood recipients with email.
Email transport
Section titled “Email transport”Invite emails use the same SMTP outbox as notification emails. SMTP must be configured for invites to be delivered. See Outbound Email (SMTP) for transport configuration.
Groups & teams (/settings/groups)
Section titled “Groups & teams (/settings/groups)”Groups let workspace Admins grant multiple users access to multiple projects in one operation. A group has a name, an optional description, an optional lead, and a list of members.
Managing a group
Section titled “Managing a group”Each group card has a Manage button that opens a management panel (a side drawer on desktop, a bottom sheet on mobile). From there an Admin can:
- Add or remove members — pick any workspace member from the searchable list; removing a member revokes the access the group conferred on them.
- Grant or revoke project access — pick a project, choose the role to confer (Viewer, Team Member, Resource Manager, or Project Manager), and grant it; each grant shows its conferred role and can be revoked.
Every change takes effect immediately (there is no separate save step) and runs the project-access cascade described below. Directory (LDAP/AD) sync of group membership is a TruePPM Enterprise capability.
Project access cascade
Section titled “Project access cascade”Linking a group to a project (via POST /api/v1/workspace/groups/{id}/projects/)
confers a project role on every current group member. This reconciliation
(reconcile_group_access) runs synchronously in the request transaction and
creates or updates ProjectMembership rows for all affected (member × project)
pairs. Board-presence events are broadcast to affected project WebSocket consumers
after the transaction commits.
The same reconciliation runs when:
- a member is added to or removed from the group,
- the conferred role for a project link is changed,
- the group is deleted (all group-conferred memberships are removed).
Direct grant wins
Section titled “Direct grant wins”Group-conferred memberships are tagged internally (source_group). If a user
already has a direct ProjectMembership on a project (one not sourced from a
group), that direct grant is never overwritten or revoked by group reconciliation.
Group membership is additive: it only removes the rows it created.
Owner cap
Section titled “Owner cap”A group can never confer the Owner project role. The conferred role is
validated to reject Owner at write time. This preserves the project last-Owner
guard — ownership must always be explicitly granted to an individual.
Group endpoints
Section titled “Group endpoints”| Method | Path | Access | Description |
|---|---|---|---|
GET | /api/v1/workspace/groups/ | Any member | List all groups. |
POST | /api/v1/workspace/groups/ | Admin+ | Create a group. |
GET | /api/v1/workspace/groups/{id}/ | Any member | Retrieve a group. |
PATCH | /api/v1/workspace/groups/{id}/ | Admin+ | Update name, description, or lead. |
DELETE | /api/v1/workspace/groups/{id}/ | Admin+ | Delete group (removes group-conferred memberships). |
POST | /api/v1/workspace/groups/{id}/members/ | Admin+ | Add a member (triggers cascade). |
DELETE | /api/v1/workspace/groups/{id}/members/{user_id}/ | Admin+ | Remove a member (triggers cascade). |
POST | /api/v1/workspace/groups/{id}/projects/ | Admin+ | Link the group to a project with a conferred role (triggers cascade). |
DELETE | /api/v1/workspace/groups/{id}/projects/{project_id}/ | Admin+ | Unlink the group from a project (removes group-conferred memberships). |
Programs (/settings/programs)
Section titled “Programs (/settings/programs)”The Programs section is a bulk-edit matrix over every program in the workspace. It is the workspace-scoped counterpart of the program’s own Projects → bulk-edit surface: instead of editing one program at a time, an admin sets a single field across a selection of programs in one atomic call.
How it works
Section titled “How it works”- Check the programs you want to change (or select all).
- Pick one field to set:
- Methodology — the default delivery model new projects in each program inherit.
Under a workspace
inheritmethodology lock this column is read-only (display-only). - Iteration label — the program’s iteration-container label; Reset to inherited clears the override so the program inherits the workspace default again.
- Slip propagation — what each program does when a cross-project dependency slips: No action, Warn only, or Block & escalate.
- Escalation days — how long a cross-project slip may persist before escalation (1–30 days).
- Methodology — the default delivery model new projects in each program inherit.
Under a workspace
- Apply. The change is all-or-nothing across the selected rows and bumps each
program’s
server_version. A selection can touch at most 200 programs per call.
Inherited values are shown distinctly from explicit overrides. Methodology and the two risk-policy fields are always-set columns (no “inherit” state).
Access
Section titled “Access”- Any active workspace member can read the program list (the matrix is visible read-only, without the edit action bar).
- Workspace Admin or Owner is required to apply a bulk change.
Endpoints
Section titled “Endpoints”| Method | Path | Access | Description |
|---|---|---|---|
GET | /api/v1/programs/ | Any member | List programs (the matrix reads methodology, iteration_label, risk_slip_propagation, and risk_escalation_days). |
POST | /api/v1/programs/bulk-fields/ | Admin+ | Set one field across the selected programs in one atomic call. |
Demo data (/settings/demo-data)
Section titled “Demo data (/settings/demo-data)”The Demo data page (Settings → System → Demo data) lists every sample program bundled with this instance — the same catalog the Load demo data picker on the Programs page offers — with, for each one, its entity counts (projects / tasks / resources), file size, and a SHA-256 of the exact bytes the download serves. A Download button lets you read the raw JSON fixture before trusting it, and a Load button builds the sample program in one click, same as the Programs-page picker. See Sample projects & JSON import/export for what each bundled sample contains and the full load/import/export walkthrough, and Inspect before you import for verifying a download’s hash from the command line.
Two of the bundled programs build their data procedurally in Python rather than from a file — the page says so explicitly rather than letting a reader believe they have audited every sample once they have checked the downloadable ones.
Not admin-gated — on purpose
Section titled “Not admin-gated — on purpose”Unlike every other page under Workspace → Settings, Demo data carries no workspace-admin requirement — any authenticated user can open it, and any authenticated user can already call the underlying load endpoint (they become the new program’s owner). The reasoning: this page only ever discloses public, Apache-2.0-licensed files already committed to the OSS repository, so gating the page would block a non-admin from following the demo loader’s own Inspect files ↗ link — a link any authenticated user can already see — into a page they suddenly cannot open. The mutation this page exposes (loading a sample) is gated the same way it always was: by the API’s own rate limit, not by a role check on this page.
Endpoints
Section titled “Endpoints”| Method | Path | Access | Notes |
|---|---|---|---|
GET | /api/v1/programs/samples/ | Any authenticated user | The catalog: key, title, description, size, SHA-256, and entity counts per bundled sample. |
GET | /api/v1/programs/samples/{key}/download/ | Any authenticated user | The exact bytes of one fixture, rate-limited per account. |
POST | /api/v1/programs/load-sample/ | Any authenticated user | Builds the sample program; the caller becomes its owner. Rate-limited (six loads per minute per account). |
Archive / Delete
Section titled “Archive / Delete”The Archive / Delete section holds the workspace-wide actions that cannot be undone. Each requires an explicit confirmation before it runs.
Export all data
Section titled “Export all data”Builds a full archive (JSON plus attachments) of everything in the workspace — members, groups, programs, projects, tasks, baselines, and history. The export runs in the background and TruePPM emails a download link when it is ready; the link expires after a few days. Take an export before either of the destructive actions below. See Data export.
Transfer ownership
Section titled “Transfer ownership”Hands workspace ownership to another active member. The transfer demotes you to Admin in the same operation, so it is not a way to add a second Owner — a workspace has exactly one. Only an active member can receive ownership; invited-but-unaccepted users do not appear in the picker. See Roles & permissions.
Delete workspace
Section titled “Delete workspace”Permanently deletes the workspace and all of its data: every program, project, task, baseline, group, and member. This cannot be undone, and every member loses access immediately. Confirmation requires typing the workspace name exactly.
This is not the same as deleting a project. A deleted project passes through Trash and can be restored inside the retention window; a deleted workspace does not.