Javis OS
Javis OS is an Agentic AI that assists entrepreneurs in business tasks, helping them grow their work.
AI translation
Lightweight, self-hosted issue and project tracking. Docker-first, your data stays yours.
BusinessJavaScript · CSSGitHub @joshhearneOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Open Source Issue Tracker w/ ability to work with External References or completely Internal Tracking only on multiple projects.
Taken from the repository README
Loading the full README.
Read the full README ↗Internal issue and project tracking — keep every ticket in reach until it's closed.
cp .env.example .env
# Fill in required values — see Environment Variables below
mkdir -p data/uploads
docker compose up -d --build
docker compose logs -f
The stack binds to 127.0.0.1:8090. A host proxy (nginx, Caddy) or cloudflared tunnel handles TLS and forwards there. For a full server setup, see Running on a VPS.
Any Linux VPS with 2 GB RAM or more works (the frontend build needs the headroom). Commands assume Ubuntu/Debian.
1. Install Docker and point DNS at the server.
curl -fsSL https://get.docker.com | sh
Create an A (and AAAA, if the server has IPv6) record for your hostname, e.g. issues.example.com. Skip this if you'll use a Cloudflare Tunnel, which creates its own DNS record.
2. Configure and start the stack.
git clone https://github.com/joshhearne/resolvd.git
cd resolvd
cp .env.example .env
Set at least these in .env (full list under Environment Variables):
| Variable | Value |
|---|---|
POSTGRES_PASSWORD | openssl rand -hex 24 |
SESSION_SECRET | openssl rand -hex 32 |
FRONTEND_URL | https://issues.example.com |
COOKIE_SECURE | true |
RESOLVD_MASTER_KEY | openssl rand -base64 32. Store a copy off the server; encrypted data and AI keys can't be recovered without it. |
Leave the Entra / Google variables empty to start with local accounts only. Then build and start:
./scripts/build-with-version.sh # docker compose build, stamped with the git version
docker compose up -d
The stack listens on 127.0.0.1:8090 only, so nothing is public yet.
3. Put HTTPS in front. Pick one; details in Host reverse proxy.
sudo apt install caddy, then set /etc/caddy/Caddyfile to:
issues.example.com {
reverse_proxy 127.0.0.1:8090 {
header_up -CF-Connecting-IP
}
}
and sudo systemctl reload caddy.cloudflared, create a tunnel in the Cloudflare dashboard (Zero Trust → Networks → Tunnels), and route your hostname to http://localhost:8090.nginx/host-proxy.conf with Cloudflare origin certificates.4. Firewall. With Caddy or nginx:
sudo ufw allow OpenSSH && sudo ufw allow 80,443/tcp && sudo ufw enable
With a Cloudflare Tunnel, only SSH needs to be open.
5. First login. Open https://issues.example.com. With no users yet, the login page offers to create the first admin (see First-time setup).
Updating. Schema changes apply automatically when the backend starts:
git pull
./scripts/build-with-version.sh
docker compose up -d
Backups. Schedule the commands in Backup (e.g. a nightly cron job) and copy the files off the server. Keep RESOLVD_MASTER_KEY with them, but stored separately.
| Variable | Required | Description |
|---|---|---|
POSTGRES_PASSWORD | yes | Postgres password |
SESSION_SECRET | yes | openssl rand -hex 32 |
FRONTEND_URL | yes | Public base URL (e.g. https://issues.example.com). Required for OAuth callbacks and inbox webhooks. |
COOKIE_SECURE | yes | true when behind HTTPS |
PORT | no | Backend port (default 3001) |
UPLOADS_DIR | no | Attachment dir (default /data/uploads) |
MAX_UPLOAD_MB | no | Max attachment size (default 50) |
COMPOSE_PROJECT_NAME | no | Override Compose project (preserve volumes after dir rename) |
| Variable | Description |
|---|---|
AZURE_TENANT_ID | Directory tenant ID |
AZURE_CLIENT_ID | App Registration client ID |
AZURE_CLIENT_SECRET | Client secret |
AZURE_REDIRECT_URI | Login redirect (https://yourdomain/auth/callback) |
AZURE_ALLOWED_ORIGINS | Comma-separated allowed origins |
The same App Registration powers SSO and OAuth-connected email backends. For email backends, also add https://yourdomain/api/email-backends/oauth/callback as a redirect URI and grant delegated API permissions: User.Read, Mail.Send, offline_access. For inbox monitoring add no extra perm — Mail.Send covers reading too in delegated context.
| Variable | Description |
|---|---|
GOOGLE_CLIENT_ID | OAuth client ID |
GOOGLE_CLIENT_SECRET | OAuth client secret |
GOOGLE_REDIRECT_URI | Login redirect (https://yourdomain/auth/google/callback) |
Add https://yourdomain/api/email-backends/oauth/callback to the OAuth client's authorized redirect URIs for the email-backend flow. Workspace + consumer support is controlled in Admin → Authentication (google_workspace_domain + google_allow_consumer).
| Variable | Description |
|---|---|
RESOLVD_MASTER_KEY | base64 of 32 random bytes. Required when encryption_settings.mode is standard and before AI Assist will accept API keys. Generate with node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" (or click Generate master key in Admin → AI Assist → Integration). Losing this key after enabling encryption or saving any AI provider key permanently destroys access. |
| Variable | Required | Description |
|---|---|---|
VAPID_PUBLIC_KEY | for push | Public VAPID key. Generate once with npx web-push generate-vapid-keys. |
VAPID_PRIVATE_KEY | for push | Private VAPID key. Treat as a secret — never commit. |
VAPID_SUBJECT | for push | mailto: or https:// identifying the operator (e.g. mailto:[email protected]). |
When unset, push silently no-ops; in-app and email channels are unaffected. Subscriptions live in push_subscriptions. HTTPS is required for browsers to register a service worker — Cloudflare Tunnel or a real cert works.
| Variable | Description |
|---|---|
INBOUND_WEBHOOK_SECRET | Required header value on POST /api/inbound/generic (X-Inbound-Secret). When unset, the endpoint refuses with 503. |
INBOUND_REPLY_TO | Optional. When set, vendor outbound emails carry this address as Reply-To so vendor replies route to the inbound pipe. |
GMAIL_PUBSUB_TOPIC | When using Gmail inbox monitoring: projects/<gcp>/topics/<topic>. Operators must grant [email protected] Publisher on the topic. |
GMAIL_PUBSUB_TOKEN | Optional shared secret on ?token= for Pub/Sub Push subscriptions. |
When no email backend account is connected via the UI, the app falls back to the legacy env-driven config:
| Variable | Description |
|---|---|
MAIL_FROM | Sender address for Graph app-only flow |
SMTP_HOST/PORT/USER/PASSWORD/SECURE/FROM | SMTP fallback |
GOOGLE_SERVICE_ACCOUNT_JSON | Workspace service account JSON for Gmail API fallback |
Recommend connecting an OAuth backend through /admin/email-backends instead of using these — Microsoft and Google are deprecating SMTP basic auth and shared-credential flows.
Configurable at runtime in Admin → Authentication:
google_workspace_domain is set; google_allow_consumer widens to consumer Gmail.MFA (TOTP) can be enforced per role. Recovery codes generated on enrollment.
Fresh install with local auth enabled shows a Create Admin Account form. First user becomes Admin. Subsequent users are invited by Admin or Manager.
| Role | Permissions |
|---|---|
| Admin | Everything — branding, auth, encryption mode, support grants, email templates, email backends, status workflows, all Manager actions |
| Manager | Projects, users, tickets, exports, invites, vendor companies/contacts, comments + attachments, vendor-visible comments, delete any comment |
| Tech | Handler tier between Manager and Submitter. Edit any ticket they have project access to, manage assets + custom-field values, edit KB articles, post handler-only notes, receive assignment + escalation pages. Cannot configure org settings, branding, auth, or run bulk admin actions. |
| Submitter | Create tickets, comment, follow, upload attachments |
| Viewer | Read-only |
| Support | External support principals — every request blocked unless an active grant exists (see "JIT support access") |
Tech is the v0.7.0 addition — the role NinjaOne / Action1 / ConnectWise customers expect for the day-to-day workstation operators who shouldn't be Admin but need to do more than file tickets.
WEB-0042). The same prefix powers email-to-ticket auto-create.internal_ref from the new project's counter; vendor contacts detach (vendor scope is project-bound).@mentions autocomplete/resolution and the "add follower" picker — Inherit (org default), Restrict to project members, or Open to all users. Org defaults live in Admin → Branding → Cross-project visibility (both default ON / restricted). Admins (global role) bypass these gates.Internal statuses ship with semantic tags that drive workflow logic (admin can rename the names without breaking behavior):
| Tag | Default name | Role |
|---|---|---|
| none | Open | Initial |
in_progress | In Progress | Active work |
awaiting_input | Awaiting Input | External block (waiting on vendor / customer) |
on_hold | On Hold | Internal block (team can't proceed) |
pending_review | Pending Review | Awaiting verification |
resolved_pending_close | Resolved | Verified, in grace period before auto-close |
| none (terminal) | Closed | Done |
reopened | Reopened | Resumed after closure / auto-reopen |
Advance button on the ticket status card walks the chain Open → In Progress → Pending Review → Resolved → Closed. Side states (awaiting_input, on_hold, reopened) are skipped from the next step; from any of them the button resumes to in_progress.
Any internal status with semantic_tag='resolved_pending_close' carries an auto_close_after_days value (default 3 on the seeded Resolved row, edited per status under Admin → Statuses). Tickets sitting in such a status past the grace period get promoted to the kind's terminal status by an hourly cron, with an audit row.
Inbound email replies during the grace window, and web UI comments on any terminal ticket, auto-reopen the ticket unless the body matches the editable gratitude phrase list (also under Admin → Statuses). "Thanks" → leave alone. Anything substantive → flip to Reopened. This covers the case where a ticket is closed in someone else's name and the actual reporter follows up with a still-open issue. Auto-reopens also fire a ticket_reopened vendor outbound email if the ticket has attached contacts and they have been previously contacted.
Each user has a 6-row × 3-column preference matrix at Account → Preferences → Notifications. Rows are event types (assignment, mention, comment, status change, pending review, follow-up). Columns are channels (in-app, email, push). Toggle any cell to opt in or out per channel.
pending_review and follow_up rows are server-side LOCKED_ON: in-app + email always fire and bypass the digest cadence — these are action-required events that shouldn't be silently batched. Push remains opt-in even for locked rows.
Email digest cadence lives next to the matrix as a per-user dropdown: instant (default) sends each notification immediately; hourly / 12h / daily buffer to the notification_outbox table and a 5-minute scheduler tick flushes ripe rows into a single digest email per user, grouped by ticket; off suppresses notification email entirely (in-app + push still fire if those channels are on).
The bell tray itself is generic — it renders whatever the matrix populated. Mention notifications scroll to and flash the specific comment so the user lands on the exact context, not just the page.
Optional, opt-in bring-your-own-AI rewrite for comments + ticket text. Each user configures their own provider in Account → Preferences → AI Assist — Resolvd ships the integration surface, you ship the API key. Provider, endpoint, model, default tone, default verbosity, and the API key (encrypted at rest via the standard envelope wrapper) all live per-user.
Admin must enable AI Assist before users can save keys. API keys (user and org) are encrypted at rest with
RESOLVD_MASTER_KEY. The feature stays disabled org-wide until that env var is set. Easiest path: visit Admin → Integrations → AI Assist → Integration — when the key is missing, a banner offers a one-click Generate master key button. Copy the value, paste into.env, restart the backend, and AI Assist comes online. The server never persists the generated key, so back it up separately (password manager / KMS); losing it after any AI keys are saved makes them unrecoverable.
Three adapters out of the box:
| Adapter | Notes |
|---|---|
openai | OpenAI Chat Completions endpoint. Compatible with Azure OpenAI, OpenRouter, vLLM, LM Studio, and anything else speaking /v1/chat/completions. |
anthropic | Anthropic Messages API (Claude models). |
ollama | Self-hosted Ollama. No API key required by default. |
New providers drop in by adding a file under backend/services/aiProviders/ and registering it in index.js.
Each provider ships a curated recommendedModels list (cheap / balanced / heavy tiers) grouped by an admin-friendly tier system. A Refresh from provider button on the model dropdown pulls the live model catalog via the provider's /v1/models (or /api/tags for Ollama) using your saved key. Per-provider helper banner with deep link to the provider's key console makes setup explicit (OpenAI: platform.openai.com/api-keys, Anthropic: console.anthropic.com/settings/keys, Ollama: ollama.com/library).
A ✨ AI button shows up on any composer wired with an aiSurface prop — internal comment composer, vendor comment composer (auto-flips when "Share with vendor" is on), ticket title (single-line surface, terse-default), ticket description (rich), admin email-template Subject + Body, canned response body, project description. Click opens a preview-before-send modal with tone (7 options), verbosity (3 options), and an ELI5 toggle (Admin/Manager only). The model output renders side-by-side with the original — Apply commits, Re-roll generates a new variant, Cancel discards.
Project context glossary: at Admin → Integrations → AI Assist → Project contexts, admins author a markdown blob per project (8KB cap) covering its sites, integrations, and lingo. The blob gets prepended to every AI rewrite fired from a ticket in that project so the model uses those names verbatim. Three layers of opt-in (org admin / project admin / per-user) all must be ON for the context to ship with a call. Per-comment badge shows "project context applied" when the blob fired.
Org-managed AI (the lock + BYOK story): at Admin → Integrations → AI Assist → Integration, the org can set its own provider/endpoint/model + encrypted org API key. The Permissions section controls how users interact with the org config:
Usage badge: every AI-rewritten comment + ticket description carries a compact ✨ AI pill inline. Hover for the full readout (provider, model, tokens, tone, verbosity, project context applied). Click copies the metadata block to clipboard — useful when reporting issues back to admin or support.
Friendly provider errors: adapters wrap non-ok responses + fetch errors in a ProviderError carrying a kind-aware friendly message (auth, billing, rate_limit, model_not_found, bad_request, server_error, network). Rendered inline with an expandable "Provider details" disclosure carrying the raw upstream body for debugging.
Template-aware: when the input contains {tag.path} placeholders (the email-template substitution syntax), the prompt explicitly tells the model to preserve every {...} token verbatim and reword only the surrounding prose.
Layered protections on the local-login path:
Admins access forensics at GET /api/security/login-attempts[/summary] — raw log + per-IP and per-email aggregates. A dedicated UI page is queued for v0.6.1.
Any internal status with semantic_tag='pending_review' accepts a follow-up reminder (1–90 days, default 3). Click "Schedule follow-up" on the ticket; an in-app notification + email fire when the timer elapses. Status advancement is blocked while a reminder is pending — cancel it or wait for it to fire (then ack via the bell tray) before moving on. Another reminder can be scheduled afterwards.
Two clocks per ticket: response (closes when someone other than the submitter posts a non-system comment) and resolve (closes when resolved_at is set). Targets come from sla_policies keyed on priority (org default) or priority + project_id (project override beats default). Default policies seeded P1 30m/4h, P2 1h/8h, P3 4h/24h, P4 8h/72h, P5 1d/7d — admins tune them at Admin → SLA policies.
Both clocks pause when the ticket transitions into a status tagged awaiting_input or on_hold (vendor / customer wait time doesn't count against you), and resume on transition out — the due-at timestamps shift forward by the paused duration. A 5-minute breach scheduler flips the breached flag + stamps the breach timestamp + fans out via the sla_breach notification event (in-app + immediate email — bypasses the digest cadence since breaches are action-required) to assignee + followers + submitter, deduped.
The dashboard gets a SLA — Month to date card: stat tiles for MTD response/resolve breach counts, currently-breached count (clickable to a filtered ticket list), open-with-SLA-clock count, plus a per-project breakdown table when MTD breaches exist. Admin / Manager see "All projects"; Submitter / Viewer see only their project_members rows. Empty-state collapses to a single line.
Per-priority + per-project step chains that fire on the four SLA triggers (warning_response, warning_resolve, breach_response, breach_resolve). Configured at Admin → Escalation policies. Each step carries a non-empty actions[] array — multiple actions on the same step fan out together. Action kinds:
notify_assignee — DM the current owner. NULL-safe (no-op if unassigned).
notify_user — DM a specific user.
notify_role — DM every active user in a role (Admin/Manager/Tech) scoped to the triggering ticket's project — Org-Wide steps don't broadcast across every project.
reassign_user — set assigned_to to a specific user.
reassign_role — set assigned_to to the first active agent in a role on the ticket's project (is_agent = TRUE).
reassign_agent — defer to the project's assignment policy (round-robin / lowest-case-load), excluding the current assignee. Falls back to any other active project agent if no policy or the pool is empty.
bump_priority — raise the ticket's urgency to surface it on boards / queues. levels (1–4) is how many tiers to escalate toward more urgent (P3 with levels: 1 becomes P2). floor (1–5, default 1) is the most urgent tier the action is allowed to reach. Writes both effective_priority and priority_override so a later impact/urgency edit recomputes against the bumped tier instead of reverting it. Audit row recorded with action priority_bump_escalation.
Cascade prevention: the first time a bump fires on a ticket, tickets.escalation_priority_snapshot is locked to the original priority. The chain matcher uses COALESCE(escalation_priority_snapshot, effective_priority, 3) thereafter — a bumped P3 → P2 ticket still matches P3 chain rows, so it doesn't fall into P2's chain on the next tick. Per-step dedup (escalation_steps_fired) still prevents a single step from re-running. The snapshot is not cleared by admin re-priority; once a ticket has been escalated, its chain identity is its original tier.
Matching is additive: an Org-Wide step (project_id NULL) and a project-scoped step both apply if both exist — this differs deliberately from sla_policies / assignment_policies (which pick one or the other). The priority_op column on steps (=, <, >, <=, >=) lets one row cover a range of priorities. delay_minutes is the grace period after the trigger timestamp before the step fires.
@ in the comment box to open a dropdown scoped to members of the ticket's project. Up/Down to navigate, Right arrow or Enter to inject the token, Escape to dismiss. Token format: @first.last derived from the user's display name. Matched users auto-follow the ticket and receive an in-app notification + best-effort email (gated by recipient's email_on_comment preference).internal_ref and detach vendor contacts (mirrors single-move semantics).WEB-0042), title, or description — no more guessing numeric IDs. Inline from a ticket (the current ticket is pre-filled as one side; pick the partner) or standalone via Admin → Merge tickets with both sides empty. Direction is chosen with a "Swap winner ⇄" toggle, not by which ticket you opened first. Comments, attachments, audit history, vendor contacts, and followers reassign to the winner; loser is closed with a pointer.Admin / Manager / Tech see a Notes tab on every ticket. Notes are project-handler-only internal commentary — separate from comments, never visible to submitters or vendors, never fanned out via email/push to non-handlers. @mentions inside notes resolve only against active agents on the ticket's project. Useful for triage scratchpad ("checked DNS, ServiceNow IP responding"), shift handoffs, or anything that shouldn't bleed into the comment thread the submitter reads.
The ticket list landing page is filter-driven, not sidebar-driven. Header carries a Mine quick-toggle, a Filters button (date range / projects / statuses / priorities / Flagged / Has fix / sort), and a saved-views dropdown. The breadcrumb chip below the title renders a plain-English summary of the active filter set (Active · last 60d · all projects · P1/P2). The left rail is a per-user Recently opened history (capped 20, deduped, populated on row click + TicketDetail mount) — that's the navigation surface; the filter modal is the query surface. Default landing: active (not Closed) · last 60 days · sort priority P1 → P5 with updated_at as the tie-breaker. URL ?preset=open|in_progress|awaiting_mot|pending_review|flagged|closed|mine|sla_breached overlays on the persisted filter once per nav (so dashboard tiles + email links still deep-link). Backend accepts CSV project_id / internal_status / effective_priority, plus assigned_to=me (resolved server-side so saved-view URLs stay user-portable) and since=ISO.
Admin / Manager can post the same comment to many tickets at once from the bulk-edit action bar. Mirror of the existing bulk-status / bulk-assignee / bulk-project flow. Each post goes through the same comment pipeline as a single post — @mention resolution, vendor visibility, follower notifications. Skipped tickets surface in the toast.
/dashboard lands on a wide grid of status tiles, a Priority Distribution bar chart, the SLA card, an Active alerts widget (Admin / Manager / Tech), a Time-in-status report, plus Recent Activity + Pending Review.
A single Filters button (top right of the dashboard header) opens a modal that drives every module on the page: date range (7 / 30 / 60 / 90 / 180 / 365 / all-time), projects multi-select, internal-status multi-select. Selection persists in localStorage.resolvd.dashboardFilters.v1. Active filters render as chips above the modules with × quick-clear and a Reset link. Submitter / Viewer requested project_id is intersected with their project_members set so they can never widen past their access.
Pending Review tile bypasses the status filter (its whole purpose is that one status). SLA dashboard skips the status filter (SLA scope is inherently breached / open).
/inventory is Resolvd's CMDB-lite. The assets table carries id, hostname, fqdn, asset_type_id, company_id, project_id, source (action1 / screenconnect / manual), source_id, last_seen_at, attributes JSONB, and an encrypted attribute_enc payload for fields tagged sensitive at the type level. asset_types + asset_type_fields drive per-type schema (laptop / server / printer / network device / generic — admin-extensible). Tickets gain asset_id FK; ticket detail renders the asset's hostname instead of an opaque #id.
Admin / Manager / Tech can create / edit / archive assets from the inventory page. The asset-type picker drives which fields appear; each asset_type_fields row carries kind (text / number / select / multiselect / date / boolean / sensitive), required, default, options (for select), and a sort_order. Sensitive fields land in the encrypted column so they don't show up in plaintext database dumps. Admin → Custom fields also exposes entity='asset' custom-field defs that surface across asset types.
When an external_alert_source with preset='action1' has its Feed inventory module capability enabled, Resolvd periodically polls Action1's REST API for endpoint inventory (hostname, OS, last-online, installed software). Reuses the encrypted api_token_enc storage pattern on the source row. Polling cadence is bounded by services/action1RateLimit.js (token bucket per source) + 429 retry-after backoff so we don't trip Action1's API limits at scale. On-demand software inventory sync runs from the asset detail page for any computer-type asset. Hudu-style org → company mapping handles the MSP silo case where one Action1 tenant feeds multiple Resolvd companies; per-source company override pins all unmapped assets to a default.
The inventory list flags assets with last_seen_at older than 14 days as Offline. Filter pill on the list narrows to offline-only.
software_aliases maps vendor-specific names ("Microsoft 365 Apps for Enterprise" / "M365 Apps" / "Office 365") to a canonical product so software reports don't fragment per vendor. Admin manage page at Admin → Software aliases. Action1's ReportRow {fields:{Name,Version,...}} shape is unwrapped on ingest so we don't store the wrapper in the DB.
Each asset row surfaces patch counts (critical / other) and vulnerability counts pulled from the source's reports endpoint. Drill-in on the asset detail page lists the open CVEs and pending patches.
GET /api/assets/export.csv?… streams the current filtered view as CSV. The :id(\\d+) digit constraint on the asset-detail route prevents /inventory/export.csv from colliding with the :id param.
/kb is per-project rich-text documentation with version history, tagging, and ticket linking. Every project owns its own articles — editors author them in BlockNote (block-based editor on top of ProseMirror), readers see a rendered surface that matches the Resolvd theme.
kb_articles carries project_id + slug (unique per project), title, content_json (BlockNote document), content_text (plain-text mirror used for FTS + summaries), status (draft / published / archived), author_id, last_edited_by, view_count, tags TEXT[], keywords TEXT[]. kb_article_versions snapshots every save with an optional change_summary; the API can restore any past version (writes a fresh version row marked Restored from vN). ticket_kb_links is the ticket↔KB junction with kind manual / suggested_accepted / system.
Admin / Manager / Tech can create + edit + archive articles. Editor surfaces a title input, slug input (auto-generates from title), status dropdown (draft / published / archived), change-summary field, and the BlockNote canvas. Tags and keywords are chip inputs — both are lowercase / length-capped / dedup'd. Keywords differ from tags in that they don't surface as filter chips on the index, but they boost the trigram similarity score when matching an article to a ticket (use them for SKU codes, model numbers, error strings that would clutter a tag chip but are searchable signal).
/kb/:projectId is the article index with a search input, status filter (draft / published / archived), and tag filter chips above the list. Multiple tags AND together via tags @> $1::text[]. Article rows show their own tag pills (brighter shade when part of the active filter). Article detail at /kb/:projectId/:slug renders the BlockNote document read-only and surfaces version history + archive controls for editors. Per-user starred projects shared between Projects + KB.
The ticket Resolution tab carries a Knowledge panel. The suggestion ranker uses pg_trgm similarity over the article's title || array_to_string(tags, ' ') || array_to_string(keywords, ' ') vs the ticket's title. Auto-surface fires on ticket open for high-confidence matches (score > 0.4 by default). Accepting a suggestion stamps a kind='suggested_accepted' link; manually picking from the picker stamps kind='manual'; ticket-promote-to-KB stamps kind='system'.
POST /api/kb/from-ticket/:id (Admin / Manager only) drafts a new KB article seeded from the ticket's title, description, and resolution_summary. Resulting article is status='draft' so an editor reviews it before publish; a kind='system' link back to the source ticket is recorded. At ticket close time, a small nudge prompts the closer to capture a one-line resolution summary — this drives the "Fix applied" ticket-list filter (resolution_summary IS NOT NULL OR a ticket_kb_links row exists). KB shortcut button on ProjectDetail opens the project's KB directly.
/alerts is the deduped state-machined alerts page — distinct from external_alert_event (the immutable audit log per vendor webhook). The alerts table is one row per (source_id, external_event_id) pair, transitioning firing → recovered as the vendor fires + clears. Alerts can carry an endpoint_hostname that links to an inventory asset when the host is known to Resolvd.
alert_rules per source decide which firing alerts auto-promote to tickets. Each rule combines severity threshold, source preset, optional title regex, and an action (promote_ticket / notify_only / ignore). Manual promote from the alert detail page records a kind='manual' audit row.
The dashboard's Active alerts card lists the top 8 firing alerts (severity badge, source label, ticket ref where promoted). Visible to Admin / Manager / Tech.
assignment_policies per project govern auto-assign on ticket create and on escalation reassign_agent actions. Strategies: round_robin (across an active-agent pool), least_open_tickets (lowest current case load), or fixed (a single user). Configured at Admin → Assignment policies. services/assignmentPolicies.js is the resolver — used by ticket POST + by escalation reassign_agent to defer agent selection back to the project's policy rather than hardcoding the pool in the chain step. Excludes the current assignee on reassign so a stuck ticket actually moves.
Account settings → Preferences exposes per-user toggles. Store: users.preferences JSONB with merged defaults from /api/users/me/prefs.
| Pref | Default | Effect |
|---|---|---|
scope_follows_filter | on | "+ New Ticket" from a filtered list preselects that project |
ctrl_enter_to_post | on | Ctrl+Enter posts a comment |
auto_follow_on_comment | on | Posting a comment auto-follows the ticket |
confirm_before_close | off | Post & Close prompts before firing |
default_ticket_sort | Recently updated | Initial sort on the ticket list |
email_on_comment | on | Email on followed-ticket comments |
email_on_status_change | on | Email on followed-ticket status changes |
email_on_assignment | on | Email when a ticket is assigned to you |
push_on_assignment | off | Browser push when a ticket is assigned to you (requires enabling browser notifications first) |
push_on_mention | off | Browser push when you're @mentioned in a comment (requires enabling browser notifications first) |
date_style_override | inherit | Override org date style (iso / us / eu); empty = follow Branding admin |
time_style_override | inherit | Override org time style (iso / 12h); empty = follow Branding admin |
timezone_override | inherit | Override org timezone (any IANA zone); empty = follow Branding admin |
compact_mode | off | Tighter padding + smaller font |
Per-device opt-in lives in Account settings → Preferences → Browser notifications. First enable prompts the browser for notification permission and registers a Web Push subscription against the backend (multi-device — each browser is its own row in push_subscriptions). Stale endpoints (HTTP 410 Gone from the push service) are auto-pruned on next send. Requires VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, and VAPID_SUBJECT env vars; generate once with npx web-push generate-vapid-keys.
Resolvd's CRM is multi-modal. Admin → Branding → Company modes toggles which kinds are active:
| Kind | Default | Use case |
|---|---|---|
| Vendor | on | External party you escalate to (printer vendor, ISP, software support). Project-scoped; drives existing vendor outbound + inbound flows. |
| Customer (MSP) | off | External party you serve. One customer can map to multiple projects (helpdesk + infra + security). Off by default — flip on for MSP installs. |
| Internal | on | Your own org units (Internal IT, DevOps, departments). Members are Resolvd users, optionally pinned to a Location. |
Admin → Companies is master-detail (account list left, selected company detail right). Detail tabs vary by kind:
Available on all kinds. Track sites with name, optional location_code (HQ, EAST), address, timezone, phone, is_primary, and use_extensions.
When a contact is created against a location whose use_extensions=true AND has a phone number, the contact form locks the phone field to the location's number and asks only for an extension. If use_extensions=false or there's no location phone, you provide a DID phone manually. Contact rows render as (555) 123-4567 ext. 1234.
Locations are soft-archived — deleting preserves contact location_id references.
Internal kind tracks which Resolvd users belong to that org unit, with optional location assignment and a free-text role label. Single source of truth for "who's on the IT team" / "who's in DevOps".
Each internal company carries an auto_add_domains list (e.g. acme.com, acme.co.uk). On any user activation event — SSO first login, invite acceptance — Resolvd matches the user's email domain against every internal company's list and auto-adds matching users to company_members. Idempotent. Saving an updated domain list also retroactively syncs existing matching users.
Match is exact apex (no subdomain wildcards). List both acme.com and dev.acme.com if you need both.
External-kind companies (vendor + customer) carry contact lists:
support@, helpdesk@, noreply@, …) are rejected at write time to prevent reply loops with the vendor's helpdesk. The list is extensible per-workspace via auth_settings.email_blocklist.Comments marked Share with vendor by an Admin/Manager fire an outbound email per attached contact, rendered through an admin-editable template (see "Email templates"). Outbound carries:
Auto-Submitted: auto-generatedX-Resolvd-No-Reply: 1Reply-To: $INBOUND_REPLY_TO when configured (defaults to the connected mailbox's from_address so replies always loop back to the system)so vendor helpdesks don't auto-reply and reflective loops are dropped on ingest.
From identity. The From address is always the connected mailbox (e.g. [email protected]). The acting human rides as the display name in the Actor via SiteName convention — "Josh Hearne via Resolvd" <resolvd@…>. Outlook, Gmail, and anti-spoof gateways (Inky VIP, Mimecast Impersonation Protect, Proofpoint) recognize via as legitimate proxied mail and don't override the display name with a directory match. No Exchange "Send As" permission required.
Reply-above-this-line marker. Every vendor outbound prepends a visible --- Type your reply above this line — ticket {ref} --- divider. The inbound parser cuts at the marker so quoted history, mail-client headers, and signatures drop automatically — the comment ends up as exactly what the vendor typed.
Comment pills. Vendor-visible comments show a brand-colored TO VENDOR pill. Inbound vendor replies (auto-ingested via email match) show a FROM {company} pill in a deterministic per-vendor color hashed from the company id — same vendor, same color across renders, sessions, and users. Hue spread is theme-aware (pastel + dark text on light, muted dark + light text on dark) so multiple vendors on one ticket stay readable.
Images attached to the ticket are included as file attachments only on the new_ticket and new_comment events. Status-change and resolved emails send body text only — keeps the inbox light when a ticket churns through several states (Graph, SMTP, and Gmail backends all supported).
The new_ticket event is not fired automatically on ticket creation. An Admin/Manager must click the Notify Vendor button on the ticket detail page to send the initial vendor notification. This gives staff a chance to review the ticket before contacting the vendor.
Status-change and resolved notifications are only sent if the vendor has already been contacted (i.e. a new_ticket or vendor-visible comment outbound succeeded at least once — stamped on tickets.vendor_notified_at). Attaching contacts to a ticket does not automatically enroll them in status updates.
Each company in Admin → Companies has a notification preferences panel controlling which automated emails that company's contacts receive:
| Toggle | Default | Behaviour |
|---|---|---|
| Status change notifications | on | Fires on status_change events |
| Filter to specific statuses | (all) | Uncheck "All statuses" to select individual status names |
| Ticket resolved | on | Fires on ticket_resolved |
| Ticket reopened | off | Fires on ticket_reopened (auto-reopen or manual reopen) |
new_ticket and new_comment always send regardless of company preferences — those are explicitly triggered by staff.
When an Admin or Manager sends a vendor-visible comment or clicks Notify Vendor, the system prompts a Send As choice so vendor correspondence stays attributed to a real person.
Ticket has a submitter (and the actor isn't them):
Ticket has no submitter (e.g. imported tickets, omitted submitter field):
submitted_by is backfilled and the email goes out under their name. Best for cleaning up imports.Goal: drop the rate of vendor mail leaking out under the system's MAIL_FROM fallback.
Each ticket has an Auto-mute vendor replies toggle. When on, matched vendor replies still land in the thread but with is_muted=true; the UI collapses them into a "N muted vendor replies" bucket. Followers don't get paged.
A daily digest at the configured local time (default 15:00 in auth_settings.muted_digest_timezone) catches up every follower with a summary of muted replies in the prior 24h. Admin/Manager can un-mute any single comment to bring it back into the main thread.
Authorized internal users (Admin/Manager/Submitter) can create tickets by emailing the inbound mailbox with a subject prefixed #PREFIX:
To: [email protected]
Cc: [email protected]
Subject: #WEB Login button crashes on submit
Body: Steps to reproduce: ...
Behaviour:
#WEB matches the project with prefix WEBreject_reason: sender_not_authorized.-- , "On wrote:", Outlook quoted headers, mobile sigs) before becoming the ticket descriptionreject_reason: unknown_cc:... for admin curation.Before auto-creating, the inbound flow checks for duplicates:
reject_reason: possible_dup:TICKET_REF so an admin decides.Anything that doesn't match the #PREFIX rule, or fails project/sender checks, falls through to the existing manual-match queue at Admin → Inbound.
For each connected OAuth email backend, an admin can toggle Monitor inbox in /admin/email-backends:
/subscriptions resource pointed at /api/inbound/graph, validates with clientState, renews every ~70husers.watch with the configured Pub/Sub topic, resumes on historyId, renews every 7 daysThe renewal scheduler runs hourly and re-issues any subscription within 12h of expiry. Without these adapters, you can still pipe email into the system manually via the generic webhook (POST /api/inbound/generic).
Admin → Email backends lets an admin connect outbound mailboxes:
smtp.gmail.com:587, STARTTLS, 16-char app password) when SMTP is the only option.The active account is enforced single-row by a partial unique index. Token refresh runs automatically before expiry.
Each connected mailbox can be scoped to one or more projects via Admin → Email backends → {account} → Project scope. Many-to-many with per-direction toggles (send, recv).
#PREFIX in the subject; scope just authorizes the mailbox to dispatch outbound for those projects.#PREFIX auto-creates tickets in that one project. Mailbox = dedicated queue. Because this is high-power config, an Admin must approve before auto-routing activates — Manager-created single-scope assignments fire an Admin notification ("Approve mailbox scope"). Until approved, inbound falls back to the existing #PREFIX flow.sendMail({ projectId }) prefers a send_enabled scoped account for that project, falls through to the global active account otherwise. Vendor outbound, follower notifications, and assignment emails all pass ticket.project_id so per-project sender identities work end-to-end.Sender authorization on the inbound side still applies: the sender must be a Submitter+ user (Admin/Manager/Submitter). External-customer auto-create through a scoped helpdesk inbox is a separate feature (planned).
Mail-security gateways (Inky, Mimecast, Proofpoint, Avanan) inject recipient banners on inbound mail flagging external/first-time/VIP senders. The banner sits above the user's reply, so cutting at it would discard real content. Per-account banner-strip patterns let the admin remove banners inline before the body lands in the queue.
Try the gateway first. If the connected inbox is a licensed resource mailbox (no human user), most gateways let you suppress recipient banners per-mailbox while keeping malware/phishing scans active. That's cleaner than regex stripping.
When upstream suppression isn't an option, Admin → Email backends → {account} → Inbound banner stripping accepts a list of regex patterns (one per line, applied case-insensitive multi-line). Preset buttons drop in the known regex for Inky, Mimecast, Proofpoint, and Avanan.
Admin → Email templates — admin-editable subject + body per (event_type, audience) with click-to-insert tag chips:
{vendor.name} {vendor.contact} {vendor.contact_email}
{ticket.ref} {ticket.title} {ticket.description}
{ticket.status} {ticket.priority} {ticket.url}
{ticket.reply} {ticket.replies.N} (last N vendor-visible comments, 1..20)
{actor.name} {site.name} {site.url}
Internal-only comments never leak through {ticket.replies.N} — the renderer filters on is_external_visible=true.
Seeded events: new_ticket, new_comment, status_change, ticket_resolved, inbound_matched, ticket_created_via_email.
Comment bodies and ticket descriptions support GitHub Flavored Markdown (GFM). The editor has Write and Preview tabs with a formatting toolbar.
Toolbar actions: Bold, Italic, Inline code, Code block, Heading, Bullet list, Numbered list, Blockquote, Link.
Keyboard shortcuts: Ctrl+B bold · Ctrl+I italic · Ctrl+`` inline code · Ctrl+Enter post comment.
On mobile, the toolbar collapses to the most-used tools (Bold, Italic, Code, Code block, Bullet list); the full toolbar is available at sm: breakpoint and wider.
The comment box uses the same mention-aware textarea as before — @mention autocomplete works inside the markdown editor.
A built-in Help page (/help, linked in the nav) provides role-aware documentation for every section of the app:
<HelpScreenshot> wrapper — real PNGs live under frontend/public/help/<slug>.png and fall back to a dashed placeholder when an image is missing (useful while shots are being authored or reshot)Resolvd's "support deposit box" model. Users with role Support are blocked on every API request until an Admin approves an active grant.
expires_at → grant lapses automatically when the clock crosses expires_at. Admin can revoke at any time.support_access_log and is visible at Admin → Support access → Access log.scope='read' blocks non-GET methods even with an active grant; scope='read_write' opens up.encryption_settings.mode controls behaviour:
<table>.<col>). Sensitive columns (ticket title/description/notes, comment bodies, audit_log values, attachment names + on-disk file bytes, vendor company/contact details, OAuth refresh tokens, SMTP passwords, inbound queue subject/body) all encrypt. HMAC blind index on tickets.title_blind_idx keeps word-equality search working.# 1. Generate a key and add to .env
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
echo "RESOLVD_MASTER_KEY=$KEY" >> .env
# 2. Force-recreate so .env reloads
docker compose up -d --force-recreate backend
# 3. Flip the mode
docker compose exec postgres psql -U resolvd -d resolvd \
-c "UPDATE encryption_settings SET mode='standard' WHERE id = 1;"
# 4. Backfill existing rows (idempotent; --verify on first run)
docker compose exec backend node /app/scripts/encrypt-backfill.js --verify
The backfill encrypts plaintext into *_enc shadow columns, NULLs the plaintext under standard mode, populates title_blind_idx, and encrypts attachment file bodies on disk.
Honest scope: outbound email content goes in cleartext to recipients (unavoidable — recipient has no key). Encryption-at-rest is a server-side discipline, not E2EE.
localStorage./api/branding/favicon; the dynamic web app manifest at /api/branding/manifest references it for installs. Removing the upload reverts to the default Resolvd favicon.The Docker stack does not terminate TLS. The bundled nginx service is plain HTTP only — it does not mount any certs (the nginx/certs/ path is gitignored, so a cert volume on a fresh clone would point at nothing). Terminate TLS upstream with one of:
cloudflared as a host service (systemd) pointing at http://localhost:8090 (or localhost:80 if a host nginx fans out by Host header for multiple apps). Keeps the tunnel out of the Compose lifecycle so docker compose down doesn't drop it, and lets one tunnel front several stacks.127.0.0.1:8090. Caddy config is in Running on a VPS; for nginx with Cloudflare origin certificates, see nginx/host-proxy.conf. Without Cloudflare:server {
listen 443 ssl;
server_name issues.example.com;
ssl_certificate /etc/letsencrypt/live/issues.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/issues.example.com/privkey.pem;
client_max_body_size 55M; # match or exceed MAX_UPLOAD_MB
location / {
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header CF-Connecting-IP "";
}
}
Client IPs. Login security blocks by IP, so the backend needs each visitor's real address. The bundled nginx takes it from CF-Connecting-IP when Cloudflare is in front, otherwise from the first X-Forwarded-For entry. Whatever sits in front must therefore set those headers rather than pass a visitor's own values through:
CF-Connecting-IP itself.X-Forwarded-For by default. Strip CF-Connecting-IP with header_up -CF-Connecting-IP unless Cloudflare is in front.X-Forwarded-For $remote_addr (not $proxy_add_x_forwarded_for, which keeps the visitor's value first) and clear CF-Connecting-IP as above.To check, sign in as an Admin and open /api/security/login-attempts/summary. If every attempt shows the same address, the proxy isn't forwarding it.
Postgres data lives in the <project>_pg-data volume and uploads in <project>_uploads-data, where <project> is the Compose project name: the checkout's directory name (resolvd for a default clone) unless COMPOSE_PROJECT_NAME is set. docker volume ls shows the exact names. Encrypted ciphertext backs up exactly the same as plaintext — but the master key is not in the dump. Lose RESOLVD_MASTER_KEY and your backup is unrecoverable.
docker compose exec postgres pg_dump -U resolvd resolvd > backup-$(date +%Y%m%d).sql
Restore:
docker compose exec -T postgres psql -U resolvd resolvd < backup.sql
Uploaded files (encrypted on disk under standard mode):
docker run --rm -v resolvd_uploads-data:/src:ro -v "$(pwd)":/dst alpine \
tar czf /dst/uploads-$(date +%Y%m%d).tar.gz -C /src .
Always back up RESOLVD_MASTER_KEY separately to a password manager / KMS / sealed envelope.
Admin uses a left-rail grouped sidebar (mobile collapses to a hamburger that auto-closes on selection). Manager role sees a subset (Users, Companies, Inbound email, System health, Export); Admin sees everything.
| Group | Item | Role | Purpose |
|---|---|---|---|
| People | Users | Admin/Mgr | Invite, role, MFA, status |
| Companies | Admin/Mgr | Multi-modal CRM (vendor / customer / internal) — see Companies | |
| Support access | Admin | JIT grants — approve, revoke, deny, view access log | |
| Workflow | Statuses | Admin | Internal/external status workflow |
| Canned responses | Admin/Mgr | Reusable comment templates with tag substitution + project scope — see Canned responses | |
| Merge tickets | Admin | Search-driven picker for merging two tickets | |
| Integrations | Alert sources | Admin | Inbound webhooks from monitoring tools (Zabbix today; Alertmanager + generic next) — see Alert sources |
| Inbound email | Admin/Mgr | Manual-match queue, with auto-create reject reasons surfaced | |
| Email backends | Admin | Connect M365/Gmail via OAuth, SMTP fallback, monitor inbox toggle | |
| Email templates | Admin | Tag-substitution editor + preview + test-send | |
| Site | Branding | Admin | Site name, logo, favicon / home-screen icon, accent color, company-mode toggles |
| Authentication | Admin | SSO providers, MFA enforcement, email blocklist, digest schedule | |
| Encryption | Admin | Mode reference + Standard-mode runbook | |
| Data | System health | Admin/Mgr | Scheduler heartbeats, integration last-seen, key counts, DB stats — see |
<PageShell variant="wide|standard|narrow"> (Layout.jsx no longer caps to max-w-7xl). Lists/dashboards use wide; ticket/project detail uses standard; settings forms use narrow.users.preferences.hidden_columns.tickets[]. Ref + Title are always-on. Reusable component (<ColumnPicker> + useColumnPrefs(tableKey) hook) ready for other tables.overflow-x-auto so horizontal scroll handles overflow on narrow viewports.Admin → Integrations → Alert sources turns Resolvd into a direct receiver for monitoring webhooks. No Zapier middleman.
Built-in adapters (services/integrations/registry.js): Zabbix (webhook, optional API for backfill), Action1 (REST poll — see the v0.7.0 disclaimer at the bottom of the page). Anything else uses the generic webhook intake with a tabular field-map editor that maps inbound JSON paths to Resolvd ticket fields with optional value_map lookups — no code change required to onboard a new vendor.
Each source has:
See the Alerts section above for what happens after ingest (deduped alerts page + rule-driven ticket promotion).
POST /api/webhooks/<preset>/<token> — token-auth via URL, no session. The mapper for the preset normalizes the payload. Behavior:
event_status=problem — dedups by external_ref = '<preset>:<event_id>'. Existing open ticket → severity-update comment. New event → creates a ticket in the source's default project, severity → priority via the configured map.event_status=recovery — finds the open ticket by external_ref, appends recovery comment, optionally auto-resolves to the first resolved_pending_close status.user_email — when supplied (e.g. Zabbix {INVENTORY.POC.PRIMARY.EMAIL}), Resolvd looks up an active local user by that email; matched users are stamped as both submitted_by and assigned_to, auto-followed for notification fan-out. Unmatched emails surface in the ticket description and an alert_unmatched_contact audit row so they can be re-attributed once the user signs in.UNIQUE(source_id, external_event_id, event_type) blocks dup tickets from Zabbix retries.external_alert_event with the raw payload for debugging.The source detail page renders a copy-able JS snippet for Zabbix's Webhook media type. Required parameters set in the Zabbix media-type Parameters tab:
event_id {EVENT.ID}
event_status {EVENT.VALUE} # 1 = problem, 0 = recovery
severity {EVENT.SEVERITY}
host_name {HOST.NAME}
trigger_name {TRIGGER.NAME}
trigger_description {TRIGGER.DESCRIPTION}
operational_data {EVENT.OPDATA}
event_tags {EVENT.TAGS}
event_url {$ZABBIX.URL}/tr_events.php?triggerid={TRIGGER.ID}&eventid={EVENT.ID}
user_email {INVENTORY.POC.PRIMARY.EMAIL}
Replace <your-token> in the snippet with the source's token, paste into Zabbix → Administration → Media types → Webhook → script, then create an Action that scopes to the host group(s) you want ticketed.
When the source has an api_url + api_token set, the source detail page exposes a Backfill open problems button (with optional host-group filter). Resolvd calls Zabbix's problem.get + event.get (selectHosts) + host.get (selectInventory: poc_1_email), normalizes each row to a synthetic webhook payload, and ingests via the same pipeline as a live fire. Idempotent. Bearer auth tried first; falls back to legacy auth field for older Zabbix.
API tokens grant read of all monitored data — treat as prod creds. Encrypted at rest under the workspace key.
Two new togglable columns on the ticket list:
external_ticket_ref (existing field, now exposed)external_ref (zabbix:1234567)Both render with the same NATO phonetic readback popover as the internal ref, so ops can read codes verbatim over the phone.
Admin → Workflow → Canned responses — saved comment templates for repeat replies. Two scopes (mix freely):
Each response also has a project scope (default All, or pick specific projects) so the picker on a ticket only shows relevant entries.
| Tag | Resolves to |
|---|---|
{ticket.ref} / {ticket.title} / {ticket.priority} / {ticket.url} | Current ticket |
{ticket.vendor_ref} | external_ticket_ref |
{submitter.name} / {submitter.firstName} / {submitter.email} | Ticket submitter |
{ticket.submitter} | Alias for {submitter.name} |
{assignee.name} / {assignee.firstName} / {assignee.email} | Assigned user |
{actor.name} / {actor.firstName} / {actor.email} | The user inserting the response |
{site.name} / {site.url} | Branding site name + frontend URL |
Unknown or unfilled tags pass through verbatim — the response never breaks. Insert via the 📋 Canned button next to the comment composer. Inserting increments use_count so frequently-used responses float to the top of the picker.
Admin → Data → System health — single-page operational dashboard. Auto-refreshes every 30s.
Surfaces:
muted_digest, inbox_subscription_renewal, auto_close) with a health dot (ok / stale / error / never_ran). "Stale" = last run older than 2× cadence.Manager role gets read access; Admin sees the same view.
Resolvd follows semantic versioning (vMAJOR.MINOR.PATCH):
v1.2.X) — bug fixes, security patches. Cut as needed; no migration drama expected.v1.X.0) — new features, additive schema changes. Roughly monthly.vX.0.0) — breaking schema migrations or API changes. Read the release notes carefully before upgrading.# 1. Land your changes on main (CI green).
# 2. Tag locally and push:
git tag v1.2.3
git push origin v1.2.3
.github/workflows/release.yml then:
v1.2.3-rc.1, v1.2.3-beta.0) as a pre-release..github/workflows/notify-website.yml listens for release: published and POSTs the configured deploy hook (Vercel / Netlify / Cloudflare Pages — set RESOLVD_DEPLOY_HOOK_URL in repo Secrets) so the marketing site's changelog rebuilds with the new release.
git tag v1.3.0-rc.1
git push origin v1.3.0-rc.1
Pre-release tags are flagged on GitHub and skipped by the website's "latest stable" pill but still appear in the full changelog.
Resolvd is source-available under the Functional Source License (FSL-1.1-ALv2).
Each release converts to Apache 2.0 two years after publication.
Plain-English explainer: https://resolvd.dev/license Need a different license for procurement? [email protected]
By contributing, you agree your contribution is licensed under the same FSL-1.1-ALv2 terms as the rest of the project. We use the Developer Certificate of Origin (DCO) instead of a CLA — every commit must be signed off (git commit -s). See CONTRIBUTING.md for details.
* This integration is an independent community project and is not affiliated with, endorsed by, sponsored by, or supported by Action1 Corporation. Action1 has not reviewed, tested, certified, or audited this integration for security, performance, or compliance purposes and is not responsible for its operation, availability, or use. “Action1” is a trademark of Action1 Corporation and is used solely to identify compatibility with the Action1 platform.
| Export | Admin/Mgr | Bulk PDF/print export and CSV download; toggle to include/exclude images |