Moving off OTRS/Znuny without a big-bang migration
Years of tickets, customers, queues and permissions sit in your Znuny database. Tiqora does not ask you to export them. It runs next to Znuny on the same database, takes over one job at a time, and leaves you a way back at every step until the last one.
The problem with migrating a helpdesk
In December 2020 OTRS AG announced that OTRS 6 had reached end of life, with no security updates after 1 January 2021. That ended the open-source ((OTRS)) Community Edition at version 6.0.x. Znuny picked up the codebase as a community fork. Its first LTS release, 6.0, shipped in January 2021, and the current LTS is Znuny 6.5. The 7.x line, currently 7.3, continues in parallel. Znuny is still mostly Perl and JavaScript running on a classic mod_perl setup.[1–4]
Many teams have run that system for ten years or more. The database holds every ticket and article, customer users and companies, queues, groups and roles, dynamic fields, SLAs, notification rules, GenericAgent jobs and the escalation calendars. Whatever comes next has to take all of that with it.
A classic helpdesk migration usually works like this:
- Export everything, map it onto a different data model and import it.
- Freeze the old system over a weekend.
- Switch every agent, mailbox, notification and integration at once.
- Hope nothing important was lost in the mapping.
The risky part is not the export. Everything depends on one moment. Ticket numbers, follow-up threading, history and SLA timers all have to come out right on the first try, and once mail flows into the new system the old one is out of date.
The idea: don't move the data, share it
Tiqora is a clean-room helpdesk built on Python (FastAPI) and React, with no Znuny code inside. It reads and writes the same database as an existing OTRS 6.0.x or Znuny 6.0–7.3 installation, on MariaDB/MySQL or PostgreSQL. You point it at your Znuny database, and both systems work on the same tickets at the same time.
That only works if Tiqora follows a few strict rules while Znuny is still running:
- No changes to Znuny's schema. Tiqora does not alter Znuny tables during parallel operation. Its migrations only touch its own tables.
-
New state goes into new tables only. API keys, 2FA,
drafts, AI settings and similar data live in additive
tiqora_*tables. Znuny never reads them. -
Writes look like Znuny's own. Tiqora allocates ticket numbers
with Znuny's lock-free counter algorithm and uses the configured number
generator. It writes
ticket_historyrows in the exact formats Znuny's parsers expect, recomputes the four escalation columns with the same working-time math, and sets the same search-index flags. - Background duties stay with Znuny until you hand each one over with its own switch: mail fetching, escalations, notifications, GenericAgent, automatic unlock and pending checks.
How this is tested. A schema-matrix suite loads the real upstream database definitions for each supported release on MariaDB and PostgreSQL. A golden-master suite runs real OTRS/Znuny containers from 6.0.45 up to 7.3.5 next to Tiqora on one MariaDB database, then checks that a full ticket lifecycle produces matching history rows (create, state change, move, priority, owner, note, close). The same suite covers ticket-number interleaving, checksum digits and escalation columns.
At startup Tiqora detects which version your schema belongs to (for example
znuny-6.5 or znuny-7.3) and adjusts to it. If it
cannot recognise the schema, it refuses to start instead of
failing later in production. The detected profile is shown under
Admin → System info.
Any version from 6.0 to 7.3 works. If you can upgrade first, the docs recommend Znuny 6.5 (LTS) or 7.3 as the peer. OTRS 5 and older need the upstream upgrade to 6.0+ first; Tiqora does not replace that database upgrade.
Step by step
The migration has six phases. You can stay in any phase for as long as you like, from a day to several months. Every phase before the last one can be undone without restoring a backup.
- 0BackupVerified dump + staging copy
- 1InstallTiqora on the same DB, pilot agents look around
- 2Cache syncTiqoraSync add-on or lower cache TTLs
- 3Agents moveDaily work in Tiqora, Znuny keeps the daemons
- 4Daemons moveOne flag at a time, postmaster last
- 5CutoverZnuny off, Tiqora owns the schema
Phases 0–4 and the first half of phase 5 are reversible by configuration. Schema ownership at the end is the only step that needs a backup to undo.
Who owns what, phase by phase
| Phase | Agents work in | Background jobs | Database schema |
|---|---|---|---|
| 0–2 | Znuny (pilot group tries Tiqora) | Znuny | Znuny + additive tiqora_* |
| 3 | Tiqora (Znuny still available) | Znuny | Znuny + additive tiqora_* |
| 4 | Tiqora | Moving one by one to Tiqora | Znuny + additive tiqora_* |
| 5 | Tiqora | Tiqora | Tiqora (additive migrations only) |
Phase 0
Back up, and prove the backup restores
Before anything touches the production database, take a dump and restore it into a scratch database. A backup job that reports success does not prove the restore works. The same restored copy is a good staging environment for trying out phases 1–4.
# MariaDB / MySQL: --single-transaction avoids locking a live Znuny
mysqldump --single-transaction --routines --triggers \
-h db.example.internal -u backup_user -p \
znuny_production > znuny_pre_tiqora_$(date +%Y%m%d).sql
# PostgreSQL
pg_dump --format=custom -h db.example.internal -U backup_user \
znuny_production > znuny_pre_tiqora_$(date +%Y%m%d).dump
Never start mail fetchers or outbound workers against a copy of production data. They would read your real mailboxes and write to your real customers.
Nothing has changed. Keep the dump until well after the cutover; it is the only way back from the final step.
Phase 1
Install Tiqora next to Znuny
Deploy the tiqora-api and tiqora-worker containers,
plus Redis and Meilisearch (both are in the example Compose file). Give Tiqora
its own database user with SELECT, INSERT, UPDATE, DELETE on the
Znuny database. Do not grant ALTER or DROP, and do
not reuse Znuny's credentials. Then point DATABASE_URL at the
existing database:
environment:
DATABASE_URL: mysql+aiomysql://tiqora:…@db.example.internal:3306/znuny_production
# or: postgresql+asyncpg://tiqora:…@db.example.internal:5432/znuny_production
# TIQORA_SCHEMA_OWNERSHIP stays unset for the whole parallel period
Create Tiqora's own tables and build the search index:
docker compose run --rm --entrypoint tiqora tiqora-api migrate upgrade
docker compose run --rm --entrypoint tiqora tiqora-api index rebuild
tiqora migrate upgrade only creates tiqora_* tables.
The migrations that may touch Znuny's tables sit in a separate chain that
stays locked until phase 5. Check Admin → System info to confirm the
detected schema profile matches your Znuny version.
If you use PGP or S/MIME, mount Znuny's keyring and certificate directories
into the containers. Tiqora uses the same key stores and respects the same
SysConfig switches. The customer portal is off by default; turn it on with
TIQORA_PORTAL_ENABLED=true.
Now give a small pilot group the Tiqora URL. Technically they can already edit tickets, so tell them clearly that this is an evaluation for now.
Stop the Tiqora containers. Znuny was never touched. The leftover
tiqora_* tables do no harm and can be ignored or dropped.
Phase 2
Keep Znuny's cache in step
Znuny caches tickets in memory. When Tiqora writes straight to the database, Znuny does not see the change until the cache expires. The fix is TiqoraSync, a small optional OPM add-on (GPL-3.0, Framework 6.0.x–7.3.x). Install it into the running Znuny:
bin/znuny.Console.pl Admin::Package::Install /path/to/TiqoraSync-1.1.1.opm
# OTRS 6.0: bin/otrs.Console.pl
It adds one Znuny scheduler task that runs every minute. The task reads the invalidation signals Tiqora writes alongside each ticket change and clears those tickets from Znuny's cache. Worst-case staleness in the Znuny UI is therefore about 60 seconds. If you cannot install add-ons, lowering Znuny's cache TTLs is the documented fallback.
Check it: create or edit a queue in Tiqora and confirm it shows up in Znuny without a restart.
Admin::Package::Uninstall TiqoraSync, and you are back to phase 1.
Phase 3
Agents switch, Znuny keeps the daemons
There is no global "writes on" switch. This step is a decision you communicate: tell a team that Tiqora is now their main interface for their queues. Expand team by team. Znuny's daemon keeps fetching mail, sending notifications and running escalations, so nothing about mail flow changes yet.
Tickets edited in Tiqora are stored in Znuny's format with Znuny's history rows, so an agent who opens the same ticket in Znuny sees a normal Znuny ticket.
Autonomous AI replies need system.operation_mode = tiqora_primary.
The default during parallel operation is parallel, so no
automatic customer reply can go out from Tiqora while Znuny still owns the
mailbox. AI drafts and summaries that an agent triggers still work. The one
exception is Telegram, which Znuny never handles.
Agents go back to the Znuny UI. Whatever they did in Tiqora is already valid Znuny data, so nothing needs to be repaired.
Phase 4
Hand over the daemons, one flag at a time
Each background duty moves on its own. Every Tiqora worker job has a flag in
tiqora_settings, off by default, which you switch under
Admin → Services. The worker picks up the change on its next tick
without a restart. Run each duty on one side only, or work
gets done twice: two tickets for one mail, notifications sent twice.
Always switch Znuny's side off first, then turn Tiqora's on.
Recommended order:
| # | Duty | Disable in Znuny | Enable in Tiqora |
|---|---|---|---|
| 1 | Escalation sweep | EscalationCheck cron task |
daemon.escalation.enabled |
| 2 | Event notifications | Ticket:: |
daemon.notifications.enabled |
| 3 | Automatic unlock | TicketUnlockTimeout cron task |
daemon.unlock_timeout.enabled |
| 3 | Pending states & reminders | TicketPendingCheck cron task |
daemon.pending_check.enabled |
| 4 | GenericAgent | GenericAgent cron task + scheduled job executor |
daemon.generic_agent.enabled |
| 5 | Inbound mail (postmaster) | MailAccountFetch cron task |
daemon.postmaster.enabled |
Escalations go first because the math is deterministic: a short overlap just computes the same value twice. Inbound mail goes last because a mistake there costs the most. With POP3/IMAP delete-after-fetch, two fetchers can create duplicate tickets or lose mail. Auto-responses do not get a flag of their own; they move with the postmaster. Pending reminders only reach anyone once notifications are on Tiqora's side.
A cron task is switched off in Znuny like this:
bin/znuny.Console.pl Admin::Config::Update \
--setting-name "Daemon::SchedulerCronTaskManager::Task###MailAccountFetch" \
--valid 0
# then deploy, and confirm the task is really gone:
bin/znuny.Console.pl Maint::Daemon::Summary
Some Znuny settings are marked required. For those,
Admin::Config::Update prints its usual success line and
changes nothing. Always check the deployed configuration
(Kernel/Config/Files/ZZZAAuto.pm) and
Maint::Daemon::Summary. The scheduled GenericAgent executor is
usually such a required setting, so plan its handover for the moment you
stop the Znuny daemon.
Before taking over notifications, list which events your rules actually listen to:
SELECT ne.name, nei.event_key, nei.event_value
FROM notification_event ne
JOIN notification_event_item nei ON nei.notification_id = ne.id
WHERE ne.valid_id = 1 AND nei.event_key IN ('Events', 'Recipients')
ORDER BY ne.id;
Rules carried over from older OTRS versions are often bound to the legacy
Notification* events. Tiqora emits those too, but a rule bound
to an event nobody emits never fires, even while the takeover reports
healthy runs. Also set notification.sender_email explicitly so
notifications do not go out from the queue's address.
For each duty: disable the Znuny task, set the Tiqora flag, wait at least one
interval, and check that the work happened exactly once (one ticket
per test mail, one history row per escalation event). Only then move on to
the next duty. GenericAgent jobs with NewDelete stay inactive
until you also set daemon.generic_agent.allow_delete.
For any duty: set its flag back to 0, let an in-flight Tiqora
run finish, and re-enable the Znuny task. Escalation columns re-converge on
Znuny's next run. The notification position is kept, so turning it on again
later does not resend old events. Mail that Tiqora left on the server is
picked up by Znuny's next fetch.
Phase 5
Cutover: Znuny off, schema ownership on
You get here when all six flags have run cleanly in production for a
representative period. The cutover runbook has its own preconditions. The
most important ones: a freshly tested restore, a maintenance window, and an
inventory of everything the Znuny host still does (OS cron jobs, add-ons,
calendar jobs, the outgoing mail_queue). Then:
-
Freeze Znuny's web UI. Put a 503 maintenance response on the
nginx vhost in front of
index.pl/customer.pl, and leave Tiqora reachable. -
Check the flags and stop the Znuny daemon. All six
daemon.*.enabledflags must be1. Stop the daemon and confirm it stays stopped through at least one full interval and the next scheduled job time. -
Repoint GenericInterface clients (optional). Only needed if
external integrations call Znuny's webservices. An nginx rewrite from
nph-genericinterface.plto Tiqora's/znuny-compatlayer covers the commonSession*andTicket*operations over REST and SOAP. Test each operation your integrators use. - Monitor. Watch error rates and per-service status under Admin → Services for at least an hour, or a business day for a cautious rollout.
-
Set the operation mode on purpose. Switching
system.operation_modetotiqora_primarycan activate AI auto-reply rules that were configured but inactive. Review them first. - Take schema ownership. Only now may Tiqora run migrations that touch Znuny's tables. This takes two separate switches:
tiqora ownership status
tiqora ownership enable --confirm "I have shut down Znuny"
# preflight: ticket_history idle ≥ 15 min, Znuny sessions table empty
# then set TIQORA_SCHEMA_OWNERSHIP=1 on every Tiqora process and restart
tiqora migrate upgrade
tiqora ownership orphan-report # optional, read-only
The first owned migration today only adds three composite indexes. It does not change any rows. Afterwards, archive Znuny's crontab and decommission its frontend. TiqoraSync is retired together with Znuny.
Steps 1–5 are undone by configuration: remove the nginx 503 override,
turn the flags you are returning off before restarting the Znuny
daemon, and revert the GenericInterface rewrite. If the ownership marker is
set but TIQORA_SCHEMA_OWNERSHIP is not, delete the marker rows
and nothing has changed. If owned migrations have been applied, the current
index-only migration can be stepped back with
tiqora migrate downgrade owned@-1. For anything beyond that,
the only supported way back is restoring the phase 0 dump.
What is not there yet, and what is out of scope
Tiqora covers everyday agent, customer-portal and admin work. Some things are still missing or narrower than in Znuny. Check this list against how your team actually works (as of 1 October 2026):
Not built yet
- Saved searches
- Configurable dashboard widgets
- Link overview and link-type admin
- Admin screens for sessions, SQL box, maintenance, system log
- Calendar admin and appointment rules
- Portal print view
Narrower than Znuny
- Statistics: fixed reports, not the Znuny stats framework
- GenericAgent: a subset of search criteria
- Bulk actions and user preferences
- GenericInterface edge cases
- Process conditions and actions
Out of scope
- OPM package manager
- Process designer (processes run, but are designed elsewhere)
- SysConfig edit UI (settings are read from Znuny's SysConfig tables)
- GenericInterface webservice editor and requester side
The daemon ports also have documented simplifications. Escalation "notify before" uses one fixed window instead of per-SLA percentages. Notifications send by email only and ignore per-agent opt-outs. The postmaster converts HTML mail to plain text more crudely and matches customers by login or email only. The full lists are in the compatibility notes and in the "Uncertainties" sections of the parallel-operation guide. Read them before phase 4; this is exactly where running both systems side by side helps, because you find out on real traffic and can still switch back.
FAQ
Do I need to stop Znuny to try Tiqora?
No. Tiqora runs next to Znuny on the same database and only adds its own
tiqora_* tables. Znuny, including its daemon, keeps running
until you choose to cut over in phase 5. For a first look without any
install, use the live demo. It runs on mock data in
your browser.
Which OTRS and Znuny versions are supported?
OTRS 6.0.x (including Centuran ((OTRS)) CE 6.0.x) and Znuny 6.0, 6.1, 6.2, 6.3, 6.4, 6.5, 7.0, 7.1, 7.2 and 7.3. OTRS 5 and older, OTOBO and the commercial OTRS product are out of scope. Upgrade an older install to 6.0+ with the upstream tools first. A schema Tiqora cannot recognise makes it refuse to start instead of misbehaving.
MariaDB/MySQL or PostgreSQL?
Both. The schema-matrix tests load each supported release's real schema on MariaDB and PostgreSQL. The tests against running OTRS/Znuny instances currently use MariaDB only, because Znuny on PostgreSQL is rare. If you run PostgreSQL, rehearse on a staging copy.
What about my OPM add-ons, or ITSM?
Tiqora leaves add-on tables alone, and it has no package manager, so
add-ons do not carry over. It provides no ITSM/CMDB module. The
GenericInterface compatibility layer does not emulate custom operations
such as ConfigItem*, and process actions like
ConfigItemUpdate are logged as unsupported.
The cutover runbook asks you to list everything your add-ons and custom jobs actually do and decide for each: replaced, no longer needed, or kept running as a separate service. If an add-on changed Znuny's core tables, try Tiqora on a staging copy first. We have not tested specific third-party add-ons, so we cannot promise any of them works.
Can I go back to Znuny?
Yes, at every step before schema ownership, and without restoring a backup.
Stop Tiqora, uninstall TiqoraSync, or set a daemon flag back to
0 and re-enable the Znuny task. The data Tiqora wrote is
already in Znuny's format. Once owned migrations have been applied, the
supported way back is restoring the dump from phase 0, which is why that
step comes last.
Is Tiqora a Znuny fork?
No. It is an independent reimplementation in Python and React, licensed under AGPL-3.0, with no Znuny or OTRS source code in it. Only the optional TiqoraSync add-on is Perl, and it runs inside your Znuny during parallel operation.