Platform console
The Hafrix side of Hafrix: the companies that work on the product, the links that let them register, the announcements we write to them, the people support gets asked about, the log of everything we did to a customer, and the manual itself.
Where: /platform. There is no link to it from the product — a customer workspace never shows one. You sign in at the ordinary /login form with a Hafrix staff account, and the console is where that account lands.
Important
This page is for the Hafrix team. Nothing on it is reachable from a customer workspace, and nothing on it is a tenant role: an operator, a manager, even the owner of the biggest customer gets This area is limited to Hafrix platform administrators. here.
Who can do what
| Account | In the console |
|---|---|
| Hafrix platform administrator | Tenants, Signup links, Announcements, People, Backups, Audit, and Change password |
+ the manual.edit right |
Documentation: drafts, preview, screenshots |
+ the manual.publish right |
Publish and Roll back a manual page |
| A writer token (an agent) | Drafts and screenshots through the API only — never Publish |
| Any role in any customer workspace | Nothing at all |
At a glance
The console has its own layout and shares nothing with the product: no capsule, no sections, no workspace switcher — there is no workspace to be in. One bar sits across the top and everything else is the page under it.
- Hafrix + the tag Platform — the brand, and a link back to Tenants.
- The nav: Tenants · Signup links · Announcements · Support · Documentation · People · Backups · Audit.
- Your email address on the right — a button, not a label: it opens Change password.
- The light/dark switch. Unlike the sign-in screen, the console follows whichever theme you pick.
- Sign out — closes this session on the server, not just in the browser.
Every page under the bar follows the same shape as the product: a header with the title, a hint under it, the action on the right, and a card with the table.
Key concepts
Three doors, and a token only opens one of them
Hafrix has three levels of access, and they do not overlap:
| Level | Who | What proves it |
|---|---|---|
| Platform | the Hafrix team | the staff flag on the account, a token issued for the console, and a confirmed second factor |
| Workspace | a customer's viewer / operator / manager / admin | a role in that workspace, plus the sections their group opens — see Roles & access |
| Person | anyone, about themselves | password, second factor, devices — see Account |
A console token carries no workspace, so it opens no product page — the product answers it as not signed in, and the browser sends you back to the sign-in form. A workspace token carries no console right, so it gets This token was issued for a workspace, not for the platform console. The sign-in form is the same one for both — remembering which door your address opens is not your job.
How a platform account is born
Not from the console. The staff flag is handed out by one command on the server and by nothing else:
manage.py create_platform_admin --email ops@hafrix.com
It refuses an address that is already a member of a customer workspace — an employee of a customer never becomes an employee of Hafrix — and it prints the generated password once. Change it at your first sign-in (see Change your console password); the product's own Account page is closed to you, because it belongs to a workspace and you are not in one.
Warning
Two-factor authentication is not optional here and has no grace period. The first sign-in sends you straight to the second-factor setup, and every console request afterwards checks that this session confirmed it. There is no way to "do it later" — the console is the most valuable door in the system and it has very few keys.
The console does not browse customer data
The console reads the tables that belong to us — companies, people, signup links, announcements, the log — and it does not read what belongs to a customer. Their items, jobs, invoices and settings are not here, and there is no screen that could show them.
There are exactly two exceptions, and both announce themselves. The members list on a tenant card: opening a card steps into that one company to read who is in it, and the server writes a Support lookup row into the audit before it answers. And a conversation on Support: opening one reads that customer's thread, and it is logged the same way. That is why the list of companies shows no head count and no item count — counting them would mean stepping into every customer at once, quietly.
The same rule applies to searching for a person: every search on People is logged too.
The support queue is the one place that looks across companies without a log row — it lists the number, the subject and the workspace of conversations waiting for us, and nothing of what is inside them. It is refreshed every fifteen seconds while the screen is open, and a log row per refresh would be noise, not a record. Reading the thread is what gets logged.
Two ways a workspace is born
A signup link is the normal way. We send it to a person, they click it, and they name their own company, pick its address, set their own password and land inside as its admin. We never type their password and never see it.
New tenant on the Tenants page is the other way, and it is for moving in a customer who already exists — a migration, not a sale. It creates the workspace immediately and can only hand it to someone who already has a Hafrix account.
If you catch yourself about to create a workspace and then invent a password for its owner: send a signup link instead. That is the whole reason the link exists.
Offsite backups, and why they are a screen
The database is dumped every night, verified, and kept for fourteen days — on the same disk as the database. A fire in the building takes both. Backups is where the second place is named: any S3-compatible storage (Hetzner Object Storage, Cloudflare R2, anything else that speaks S3 — only the endpoint, the region and the shape of the keys differ).
There is a second offsite copy, and it is not on this screen. The same encrypted snapshot also goes to a Cloudflare R2 bucket in the EU jurisdiction — a different provider from the storage box, so that losing one provider account or one region does not take both copies. Its bucket and its key live in deploy/.env on the server (R2_BACKUP_*), because it has to work on the day this screen and its database do not. It is always encrypted, it follows the same retention, and a failed upload is its own orange line in Telegram. deploy/backup_offsite.sh r2 show on the server tells you what it is set to.
It is a screen rather than a line in a config file because the thing it holds — an access key — expires, gets rotated and gets replaced while the service is running. Editing a file over ssh at three in the morning is not where that should happen.
Three things about it are not negotiable, and the screen enforces all three.
The secret never comes back out. You type it once; the server stores it encrypted with a key that lives outside the database, and afterwards it will only tell you set, ending 9911. There is no reveal button and no API that returns it. A dump of our own database, stolen, does not hand anyone the keys to the backups made from it.
Offsite is off until it is configured. The switch refuses to turn on while the endpoint, the bucket or either key is missing. Otherwise the screen would say on while the nightly script quietly skipped the step — and we would find out on the day it mattered.
And it says so when something later stops it. Configured once is not configured forever: rotate the server's own encryption key without re-entering the provider secret and the stored key becomes unreadable, and a passphrase that disappears from deploy/.env stops the upload just as surely. Either way Last delivery names it under Stops the upload the moment it happens, and the secret is shown as stored, but this server cannot read it any more rather than as a healthy key — instead of leaving you to work it out from a red badge the next morning.
The dump is encrypted before it is uploaded. Otherwise the provider ends up holding a complete, readable copy of live customer data; TLS protects the wire, not the shelf. The passphrase is not on this screen — it lives in deploy/.env on the server, and it has to be kept in a second place as well, a password manager or a safe. The fire that the offsite copy exists for takes deploy/.env with it, and a snapshot nobody can decrypt is not a backup.
Nothing on this screen talks to the provider except the test button. The upload is done by the backup script on the server, after the local snapshot is finished and checksummed; the screen shows what that script reported. So a value you change here takes effect at the next nightly run, and Last delivery is a report, not a live query.
A storage box is the other kind of destination. A Hetzner Storage Box is not S3: it has no bucket and no access keys, and the backup script reaches it with rsync over ssh. What it has instead is a server, a login and a password — and a password to a server is not something we ask anyone to paste into a browser. So for that provider the address, the login, the folder and the password live in deploy/.env on the server, next to every other secret, and this screen only shows them. What stays here is what is genuinely yours to decide: whether the copies go out at all, how long they are kept, and whether they are encrypted first. Test connection is off for a storage box, because only the server can reach one — the nightly run is what tests it, and deploy/backup_offsite.sh show answers on demand.
The snapshot is not the only thing that travels. A dump restores the data; it does not restore a server. So each upload also carries the files that exist nowhere else — deploy/.env, the cron schedule and the nginx configuration of the host — in an encrypted bundle under _files/, along with the dumps kept aside before irreversible operations. That bundle is encrypted even when Encrypt the dump before uploading is off, because it contains the keys to everything else; without a passphrase on the server it is not uploaded at all.
The manual has two rights, on purpose
manual.edit opens drafts, preview and screenshots. manual.publish opens Publish and Roll back. They are handed out separately by manage.py manual_access, and an agent's writer token gets only the first one — the server refuses a publish that arrives on a token even when the token's owner has the right in person.
Four eyes: whoever wrote the page is not the one who decides it goes live. Accepting a chapter of this manual is pressing Publish.
How to
Sign in to the console
Who: a Hafrix platform administrator.
- Open
/loginand enter your Hafrix address and password — the same form customers use. - Confirm your second factor.
- You land on Tenants. There is no workspace chooser, because you are a member of none.
Result: the session is open, and it is an ordinary session: it shows up in your device list and can be signed out from anywhere. It follows the same limits as a workspace session: it ends after 1 hour without anything you do — the Care queue refreshing itself on screen does not count — and in any case 24 hours after sign-in; the next click then lands on the sign-in form. Signing in from a device you have not used before sends you the New sign-in to Hafrix from {device} email.
If it fails: This account has no access to any workspace yet. means the address is not staff — it is an ordinary account with no memberships, not a console account. This area is limited to Hafrix platform administrators. means the account is real but does not carry the flag. Two-factor authentication is required for the platform console. means the session never confirmed the second factor — sign out and in again.
Find a company and open its card
Who: a platform administrator.
- Open Tenants. The list carries Company (name and address), Status, Plan, Time zone and Created.
- Type into Search to narrow by any part of the name or the address.
- Click the row to highlight it, then click it again, or press Enter, to open it (Choose how a row click behaves).
Result: the card opens with the company's name, its status badge, and the hint line address · time zone · created. Members lists everyone in that workspace: name, job title, role, and inactive for the ones whose membership is switched off.
If it fails: No members yet. is not a bug — a workspace created by hand has nobody in it until an owner is attached. And remember that opening this card is a logged support lookup: it will be in Audit under your name within the second.
Suspend or reactivate a workspace
Who: a platform administrator.
- Open the company's card.
- Under Plan and status, click Suspend (it is the red button) or Activate.
Result: the badge flips between Active and Suspended in the card and in the list, and a Tenant updated row lands in Audit with the field that changed. A suspended workspace stops for its people on their next request — not when their token expires, on the next click.
If it fails: nothing is sent to the customer. There is no email, no announcement and no warning banner — if they should know, tell them (Publish an announcement is the tool). People already signed in will simply hit a wall mid-work, so pick your moment.
Change a company's plan
Who: a platform administrator.
- Open the card, type into Plan under Plan and status.
- Click Save.
Result: the Plan column updates and Audit gets a Tenant updated row listing the change.
The same block holds Company and Time zone. Change them there and click Save — only what you changed is sent. A new name emails every admin of the workspace (who renamed it, from what, when); a new time zone moves the workspace's "today" and its nightly jobs, while issued invoices and closed months stay as they are. Inside the workspace the owner renames it and any admin changes the zone in Settings → Workspace. A zone must be a named IANA zone — Unknown time zone. otherwise.
If it fails: the plan is free text and nothing depends on it yet — it does not switch limits on or off, and no invoice comes out of it. Treat it as a label for us, not as a setting for them. The workspace address cannot be changed at all: it lives in links people have already saved.
Create a workspace by hand
Who: a platform administrator. Use this only to move in an existing customer.
- On Tenants, click New tenant.
- Fill in Company and Address — both required.
- Pick the Workspace type. Customer is a paying workspace with no end date. Pilot runs on the customer's own data for a term and is never deleted automatically. Sandbox is a throwaway showcase — see Give a customer a sandbox.
- Term, days appears for a pilot and a sandbox; it defaults to 30.
- Plan is optional; Owner email (optional) attaches an existing Hafrix account as the workspace admin and owner — the one person who can rename the workspace and hand ownership over. Left empty, the first admin who accepts an invitation becomes the owner. Time zone defaults to
Asia/Riyadh; a sandbox has none to pick. - Files region decides where the workspace's attachments, logo and support files are stored: EU (the default) or US. It is set once — changing it later means moving the files, so ask before you pick. The database stays in the EU either way. A sandbox has no choice: it is always EU.
- Click Create.
Result: the workspace exists immediately, seeded with the starter shift catalogue, and appears in the list as Active. Audit gets a Tenant created row. Nobody is emailed — not the owner, not anyone.
If it fails: This workspace address is already taken. — pick another. This workspace address is reserved. Pick another one. — the address is one of ours (api, www, admin, app, platform, hafrix, yard, mail, static, assets, dash, signup, login, auth). No account with this email yet. Send a signup link instead — it creates the account and the workspace in one step. — exactly what it says: leave Owner email empty or go to Signup links. File storage for region US is not set up here. — this installation has no US bucket yet; create the workspace in the EU only if the customer agreed to it.
Give a customer a sandbox
Who: a platform administrator. This is what you hand to someone who wants to see the product before they have data of their own.
- On Tenants, click New tenant and fill in Company and Address.
- Set Workspace type to Sandbox. Term, days appears — 30 by default.
- Leave Fill with the demo showcase on. It copies the demo workspace as it stands today: fleet, jobs, field tickets, rentals, invoices, purchasing, kits.
- Owner email (optional) adds the customer's own Hafrix account as an admin and the owner of the sandbox, on top of the four accounts below. Without it, the sandbox's admin account is the owner.
- Click Create.
Result: the dialog turns into the sandbox accounts — four addresses, one per role, and one shared password. Copy takes all of it at once; that is what you send the customer. The showcase fills in the background, and the card shows Filling the showcase… until it is ready.
What a sandbox is: a workspace whose data is ours and disposable. Two-factor authentication is never asked for in it, no matter the role. It expires on its term, goes read-only for seven days, and is then removed for good.
If it fails: the password is shown once. If it goes astray, open the card and use New password — the old one stops working immediately. If the card says Filling failed, use Rebuild showcase: filling is a copy, and a copy can simply be made again.
Tip
One sandbox is enough per customer. Inside it, the viewer sees what a viewer sees and the operator what an operator sees — that is what the four accounts are for.
What the customer sees inside a sandbox
Who: the customer, in the sandbox you gave them. Worth knowing, because it is the answer to "can they see the finance module?"
In a sandbox the profile menu carries two extra rows, next to the theme:
- View as role — viewer, operator, manager or admin. The screens, the buttons and the API all follow: a viewer sees no New button because the server would refuse it.
- Account level — Yard, Operations, Business or Business + Intel. It hides and shows whole sections exactly as a real plan would: Yard has no Jobs and no Invoices, Business has both, Intel adds Connectors.
A fresh sandbox starts at Business + Intel — everything visible. Narrowing it is one click, and it is the customer's to make.
Neither row exists in a customer or pilot workspace: there the role is given by an admin and the sections are closed by the customer's own groups.
Extend, rebuild or delete a sandbox
Who: a platform administrator. Open the sandbox card from Tenants.
- Extend adds 30 days and unfreezes the workspace in one click. Use it when the customer comes back.
- Rebuild showcase removes everything in the sandbox and copies the demo workspace again. This is how a sandbox that was created months ago catches up with the product — the copy is a snapshot, and features shipped since then are only in the fresh one.
- New password issues a new shared password for the four accounts and shows it once.
- Delete sandbox removes the sandbox and its data for good, after a confirmation.
Result: every one of these lands in Audit as a row against the workspace. Deletion is not reversible, and it is refused outright for a customer or pilot workspace — those are never deleted this way.
If it fails: Only a sandbox can be deleted — a customer workspace never is. means the type on the card is Customer or Pilot; suspend it instead. A rebuild that ends in Filling failed leaves the sandbox empty — run it again.
Send a signup link
Who: a platform administrator.
- Open Signup links and click New signup link.
- Fill in Recipient — the email address the invitation goes to, and the address the new workspace will be owned by. It is fixed at this moment and the recipient cannot change it, so a link that lands in the wrong inbox cannot register a company on someone else's address.
- Company and Suggested address are hints for the form they will see; they can overwrite both. Note is for us.
- Click Create link.
- The dialog turns into the link itself. Copy it now with Copy link, then Close.
Result: the recipient gets the You're invited to set up {company} on Hafrix email, the row appears as Pending with an Expires date fourteen days out, and Audit gets a Signup link created row.
If it fails: the link is shown once and only once — the database keeps nothing but its hash, and there is no "show it again". If you lose it, revoke the row and issue a new one. The suggested address is not reserved either: it is checked when the recipient clicks, which may be two weeks later.
Tip
Copy the link even when the email is enough. Mail sometimes does not arrive, and pasting the link into a chat is a perfectly normal way to finish the job — it is how we do it ourselves.
Revoke a signup link
Who: a platform administrator.
- On Signup links, find the Pending row.
- Click Revoke at the end of it.
Result: the status becomes Revoked at once and the link is dead; Audit gets a Signup link revoked row.
If it fails: there is no confirmation step — the click is the action, so read the row first. This link has already been used — the workspace exists. means you are too late: the company is registered, and what you want is Suspend on its card. The recipient is not told that the link was revoked.
Publish an announcement
Who: a platform administrator.
- Open Announcements and click New announcement.
- Write the Title and the Message. The message editor offers Bold, Italic, Bullet list and Link — nothing else, on purpose.
- Pick the Level, and read the hint under it: Info is a line in the notification list and interrupts nobody; Important opens in the middle of the screen until each person confirms it; Critical opens in the middle of the screen and closes only by the button.
- Pick the Audience: All tenants, or Chosen tenants and then tick the companies.
- Tick Send by email as well if the announcement must not wait for someone to open Hafrix.
- Click Publish.
Result: a toast says Sent, and the row appears with its level, audience and a People count that fills in as the fan-out lands company by company. Every active person in every addressed workspace gets a notification row from From the Hafrix team; Important and Critical also open a window in the middle of their screen. If you ticked the email, each of them also gets it as mail with your title as the subject. Audit records the publication.
If it fails: Publish stays disabled until there is a title, a non-empty message and an audience. All tenants means every workspace that is active right now — a company that registers tomorrow will not receive today's announcement, which is what you want for planned maintenance. An announcement cannot be edited after it goes out; there is only revoke and write again.
Revoke an announcement
Who: a platform administrator.
- On Announcements, click Revoke in the Status column of the row.
- Confirm — the dialog warns that It leaves the list in every tenant it reached.
Result: the rows disappear from every recipient's list and the centred window stops opening; the row here is marked Revoked.
If it fails: email cannot be recalled. If you ticked Send by email as well, revoking cleans up the list inside Hafrix and nothing else.
Find out where a person has an account
Who: a platform administrator. This is the support tool for "I can't get in".
- Open People, type at least three characters of the address into Email address.
- Click Find. The search runs on the button, not while you type.
Result: up to ten cards. Each shows the name or address, Active / Inactive, the Hafrix badge for one of us, and a line reading address · Verified: yes/no · Last sign-in: …. Under it, every workspace the person belongs to with its address and role, or No workspaces.
If it fails: Enter at least three characters of the email address. — the search will not run on two. Nobody found. means no account with that address exists at all, which is itself the answer: the person was never invited, or was invited to a different address. Every search is written to Audit under your name.
Note
That is the whole of it. There is no button here to create a person, deactivate one, reset someone's password or sign in as them. Access is taken away inside the workspace by its own admin, or by suspending the workspace.
Answer a conversation the assistant handed over
Who: a platform administrator.
An assistant answers customers inside their own workspace. Some conversations it hands to a person instead: anything about money, access or data loss, anything the manual does not cover, anything its own review would not pass — and a bug fix the autonomous lane started but could not finish. Those are what this screen holds.
- Open Support. The queue lists what is waiting for a person, newest first: Number, Workspace, Subject, Status, When, Reviewed. The dropdown switches between four views — see Check what the assistant answered.
- Click a row to highlight it, then click it again, or press Enter, to open it (Choose how a row click behaves). The thread opens underneath, exactly as the customer sees it — their messages, the assistant's, and the dashed system rows. A system row here shows its code (
guard,budget,unavailable,fix_stopped) rather than the sentence the customer reads: what matters to you is why it came here. - Write the answer and click Send, or Send and resolve if this closes it.
Result: your answer joins the thread as support, signed with your name — the customer sees a person answered. Send leaves the conversation waiting for them; Send and resolve closes it with an answer and sends them the closing email. Either way the row leaves the Needs us queue and counts as reviewed — answering is a stronger statement than looking, and nobody is asked to press two buttons for one act.
Write in the language the customer wrote in. The text goes to them exactly as typed — nothing translates it on the way.
If it fails: A CLOSED conversation cannot become… means the customer closed it themselves while you were writing. Nothing is lost; they open a new one when they need us again.
Answer from Telegram
Who: a platform administrator, from a phone.
Every new conversation and every escalation arrives as a push in our Telegram channel. Until now the push could only send you to the console, and the trip took a sign-in and a second factor before you could type a word — which is why conversations sat for a day. You can answer the push itself.
- Find the push. It carries the number, the workspace and the author's role — never the subject, never the customer's words.
- Reply to that message in Telegram and write the answer.
- Or press Mark reviewed on the message if the conversation needs nothing from you.
Result: your text joins the thread as support, signed with the staff account we answer from, and the conversation goes to waiting for the customer and counts as reviewed — the same as answering from the console. The bot replies Sent to CARE-… so you know it landed. Open thread on the same message takes you to the console when you would rather read first.
The channel is deliberately narrow. Replying and marking reviewed is all of it: you cannot read a thread, search for a conversation, resolve one or delete anything from Telegram. Resolving emails the customer, and a mis-tap in a messenger is too expensive for that; reading is closed because the customer's words are theirs and do not belong in a third party's chat. Everything else stays in the console, where a second factor is asked for.
If it fails: Reply to the message about the conversation — you wrote into the channel without replying to a specific push, so there is no way to tell which thread you meant. I don't recognise this thread — the push is older than sixty days; answer from the console. The conversation is already resolved — the customer or someone here closed it while you were typing. I don't know who to sign as — TG_REPLY_AS is not set in the deployment; an unsigned answer in a customer's thread is not allowed.
Check what the assistant answered
Who: a platform administrator.
Most conversations never reach the queue above: the assistant answers them itself and the customer goes back to work. Reading a day of that is a different job from answering an escalation, and it has its own view.
- Open Support and pick a view in the dropdown:
- Needs us — the default: escalations and fixes the lane is running.
- Answered by AI, not reviewed — the assistant replied and no one here has looked yet. This is the day's reading list.
- Everything live — every open conversation, whoever the ball is with.
- Everything, including closed — resolved and closed threads as well, newest first, up to two hundred.
- Click a row to highlight it, then click it again, or press Enter, to read the thread (Choose how a row click behaves).
- Click Mark reviewed if it needs nothing from you, or answer it — either one takes it off the list.
Result: the conversation carries who reviewed it and when, and the Reviewed column shows the time. If the customer writes into that thread again, it comes back to Answered by AI, not reviewed: the mark is a watermark on the conversation as it stood, not a permanent tick.
Every evening a summary of the day lands in the Telegram channel — how many conversations were opened, how many the assistant answered, how many its own review flagged, how many came to us, how many are waiting for a person, and how many are still unreviewed. Demo workspaces are counted on their own line and never mixed into those numbers.
Read the audit trail
Who: a platform administrator.
- Open Audit. The newest two hundred events are listed: When, Event, Who, Workspace, Details.
- Narrow with the Event dropdown, or leave it on All events.
Result: you see what the Hafrix team did to customers — workspaces created and updated, signup links created, revoked and accepted, platform admins created, support lookups, announcements published and revoked. Details spells out the meta of the row: which fields changed, which address was searched for, how many members were read.
If it fails: this log is ours and answers only for us. What happened inside a workspace — who changed an item, who invited whom — is that workspace's own audit and is not visible from here. Announcement events show up in the list but are missing from the Event dropdown, so filter them by eye for now.
Write a manual page
Who: a platform administrator with manual.edit.
- Open Documentation. Each row is one document; the four columns after it are the languages, each showing None, Draft, Published or Unpublished edits with the live version number.
- Click the cell for the language you want — that opens the editor for exactly that document and language.
- Write in Source on the left. What the reader will see on the right is rendered by the server with the very same compiler the public site uses, so it is not an approximation.
- Watch Fix before publishing and Worth a look above the editor. The first blocks publication; the second does not.
- Click Save draft. The button reads Saved while there is nothing new to save.
- Preview link saves the draft and opens the page in the real showcase layout, in a new tab. The link is signed and lives an hour — it is shareable for exactly as long as it is useful.
- To change where a page sits in the table of contents, go back to Documentation and use the arrows in the Order column — Move up in the Guide and Move down in the Guide. Arrows appear only for Guide pages: the pages that open the help center, before the product sections. A page named after a product section follows the product navigation and shows no arrows.
Result: the draft is stored against the document and the language and the row on the index moves to Draft or Unpublished edits. Drafts are a working surface: saving one publishes nothing and nobody is notified. A new order is different: it belongs to the document, not to a version, so it needs no Publish and reaches all three showcases in every language within a cache cycle. The order shows in the Guide group of the help center; pages of product sections follow the product navigation. Reordering takes a person with the manual.edit grant — a writer token cannot do it.
If it fails: Editing the manual requires the manual.edit grant — the nav shows Documentation to every platform administrator, but the right is separate; ask for it. A new document is created with New document: Slug is its address, lowercase latin and dashes, with a slash for a nested topic (reference/glossary); Section key ties it to a section of the product and is left empty for a cross-cutting topic.
Publish a manual page
Who: a platform administrator with manual.publish. A writer token can never do this.
- Open the document and the language, and read the preview. This is the acceptance.
- Clear everything under Fix before publishing — Publish stays disabled while anything is listed there, and while the draft has unsaved edits.
- Click Publish, write one line into What changed, and confirm.
Result: the draft becomes a new numbered version that can never be rewritten, the live pointer moves to it, and the page is live in all three showcases — the help panel inside the product, /help, and the public documentation site — within a cache cycle. The version records who published it, when, your note, and which build of the software was running. No email and no notification goes anywhere: publishing is our business, not a customer event.
If it fails: The page does not pass validation — fix the problems and publish again. comes with the list. The four checks are always the same: a ru or ar page must be written in its own script and not be a copy of the English one; a translation must carry exactly the anchor set the English page carries; an anchor that a live screen links to cannot be removed; and every page opens with a single # heading, with the same heading levels in the same order as the English page — a translation that lost its title would otherwise go live with no heading at all, because a showcase draws the body and nothing else. Warnings — a link to a document that does not exist yet, an image with no upload behind it — never block anything; during a wave of new chapters they are expected.
Roll a manual page back
Who: a platform administrator with manual.publish.
- In the editor, click History. Every publication is there with Version, Published, Published by, What changed and Software; the current one carries the Live badge.
- Pick two versions in the selects underneath to read the diff of their sources.
- Click Roll back on the version you want and confirm.
Result: that version becomes live again. Nothing is rewritten and nothing is deleted — the later versions stay in the history, and the rollback is just the pointer moving.
If it fails: the diff panel only appears once there are two versions. Roll back is not offered on the version that is already live.
Manage website requests
Who: a platform administrator signed in with MFA.
- Open Website → Lead pipeline. Search by name, email, company or reference; filter demo requests and support requests separately.
- Choose a stage card to filter the list. The cards count all matching requests across stages.
- Open a request. Check Email verification before following up: the visitor receives an email confirmation link, and staff are notified after the visitor confirms it.
- Choose an available next stage and add an internal note, then save. The stages are New, Qualified, Demo scheduled, Proposal, Won and Lost.
- Read the history to see who changed the stage, when, and what they recorded. Notes stay internal; saving a note does not email the visitor.
Result: the request and its history are saved independently of customer workspaces. A customer workspace administrator cannot access this console.
If it fails: a conflict means another administrator changed the request first. Reload the latest record and review it before saving again. If the public form is unavailable, check the website's Turnstile configuration; requests must not silently disappear into an email link.
Manage website pages and SEO
Who: a platform administrator with manual.edit to save drafts and manual.publish to publish or roll back.
- Open Website → Pages & SEO and select a page, or choose New page and enter its address and title.
- Write the page content in Markdown, beginning with one level-one title. Give every section heading a named anchor, following the same rules as documentation pages.
- Edit the search title and description, social title and description, and optional canonical path. Keep this page out of search results adds noindex and excludes the page from the sitemap once published.
- Save the draft. Review the saved preview and validation messages. Saving alone changes nothing on the public website.
- Click Publish and confirm. A custom page uses the document as its body; a designed page such as the home page shows it below its existing layout. Published SEO settings apply to both.
- To restore an earlier publication, choose Restore this version beside a previous version and confirm. Later versions remain in the history.
Result: the website reads the published version. Drafts remain private, and website pages do not appear in the product's help panel.
Product and mobile pages: the designed module pages are under /product/<module> and the mobile overview is /mobile. Their real product screenshots come from the studio demonstration workspace. Publishing the matching page adds editorial material and SEO settings to the designed layout; it does not overwrite the photographed interface.
Publish a product update: choose New release in Pages & SEO. Complete the address as /changelog/YYYY-MM-DD-short-title, enter the release title, and create the draft. The editor starts with sections for operational benefits and technical notes. Replace the drafting prompts, add a short description for the release card, save, preview and publish. A published dated entry appears automatically at /changelog, newest date first. Drafts and ordinary pages do not appear in the release feed. Edit or roll back a release through the same page history.
Legal review copies: /legal/terms, /legal/privacy, /legal/dpa and /legal/sla are initial review documents on the password-protected website. Keep the review status and noindex while the company reviews the contract terms. Publishing a document in the CMS is separate from accepting a commercial contract.
If it fails: fix blocking validation messages before publishing. If somebody else saved or published first, the version check rejects your stale action; review the latest draft before retrying. Reserved addresses cannot be created as website pages.
Upload a screenshot
Who: a platform administrator with manual.edit.
- In the editor, click Images.
- Put the path exactly as the page writes it into Path in markdown — the page's
img/items/01-list.pngisitems/01-list.pnghere. - Choose the File and click Upload.
Result: the screenshot is stored and every page that refers to that path picks it up. Uploading the same path again replaces the picture instead of adding a second one, which is what re-shooting a screen should do.
If it fails: images only — PNG, JPEG, WebP, GIF or SVG, up to 8 MB. Worth a look will keep listing image with no attachment until the path here matches the path in the markdown character for character. Delete image removes it from every page that used it, so check first.
Set up offsite backups
Who: a Hafrix platform administrator. Before you start: a bucket at the provider and a key pair that can write to it. For a storage box, see the short version below instead — its address and password are set on the server, not here.
- Open Backups and click Settings.
- Pick the Provider, then paste the Endpoint the provider gave you (it starts with
https://) and the Region. - Fill in the Bucket and a Key prefix —
hafrix/is a good one. Each snapshot lands in its own folder under the prefix, named by the time it was taken. - Paste the Access key and the Secret key. Leave the secret empty when you come back later to change something else: empty means keep the key you already have.
- Set Keep offsite for — the offsite copy outlives the local one, which is kept for fourteen days. Ninety days is the default. Every snapshot is kept for thirty days; after that only the first snapshot of each month stays, until this limit. So a tenant deleted today is gone from every copy within ninety days, and there is still a month-old snapshot to go back to.
- Leave Encrypt the dump before uploading ticked.
- Tick Copy backups offsite and click Save.
Result: the settings are saved, the change is written to the audit as Offsite backup settings changed — without the key itself — and the next nightly backup uploads its snapshot. Test the connection now rather than waiting for the night (see below).
If it fails: Offsite backups cannot be switched on until this is filled in: … names exactly what is missing. encryption_key in that list means the passphrase is not in deploy/.env on the server; the upload would refuse to run, so the switch refuses first. Offsite retention cannot be shorter than 14 days — a second copy that expires before the local one is not a second copy.
Send the backups to a storage box
Who: whoever has a shell on the server, and then a Hafrix platform administrator. Before you start: a Hetzner Storage Box, ideally a sub-account of one, and a folder in it for us.
- On the server, put the box into
deploy/.env:HETZNER_S3_HOST,HETZNER_S3_USER,HETZNER_S3_SSH_PORT(23),HETZNER_S3_PASSWORDandHETZNER_S3_PATH— the folder, for examplehafrix. The names are historical: the box speaks SFTP, not S3. - Make sure
BACKUP_ENCRYPTION_KEYis in the same file, and that a copy of it is somewhere else as well — a password manager or a safe. Without it nothing is uploaded; without a second copy of it nothing can be opened after the fire. - Check that the server can actually reach the box:
deploy/backup_offsite.sh showprints the address, the folder and either the free space it found or the refusal it got. - In the console, open Backups → Settings, pick Hetzner Storage Box (SFTP) as the Provider, set Keep offsite for, leave Encrypt the dump before uploading ticked, tick Copy backups offsite and click Save.
Result: the destination card names the box, the login and the folder, and the next nightly backup uploads its snapshot there — plus the encrypted bundle of host files. To send one right now without waiting for the night: deploy/backup_offsite.sh push <snapshot>, or make backup for a whole new snapshot.
If it fails: HETZNER_S3_… is empty in deploy/.env on the server names the line that is missing — the console cannot fill it in for you, and the switch refuses to turn on while it is missing. If show says the connection failed, the box's own words follow it: a wrong password, a sub-account without access to that folder, or a firewall on port 23.
Test the connection to the provider
Who: a Hafrix platform administrator, once the target is configured.
- On Backups, click Test connection.
- Read the result under Test connection.
Result: The object was written, read back byte for byte and deleted. The button writes a small object into the bucket, reads it back, compares it and removes it — a key that can only list would pass a lesser check and fail at three in the morning on the first write.
For a storage box the button stays off: nothing in the console can reach it. Use deploy/backup_offsite.sh show on the server instead — it is the same check, run from the only place that can run it.
If it fails: The provider refused the keys. — the key pair is wrong, expired or has no write permission on this bucket. There is no such bucket at that endpoint. — check the bucket name and that the endpoint belongs to the same region. The endpoint did not answer. — the address is wrong or the provider is down. The provider's own words are shown underneath, untranslated, because that is the string you will paste into their support ticket.
Check that an offsite copy comes back
Who: whoever has a shell on the server. There is no button for this: it restores a whole database and takes minutes.
Once a month, on schedule, the rehearsal takes the offsite snapshot rather than the local one — it downloads it, decrypts it, restores it into a throwaway cluster and checks the same counters. To run it by hand:
make restore-check OFFSITE=1
On the fifteenth, the same rehearsal takes the snapshot from the second copy in Cloudflare R2 instead — by hand, make restore-check R2=1.
Result: the run ends by saying the offsite snapshot restores and holds data and how many seconds the restore took, and a green line lands in the Telegram channel. Every rehearsal, passed, failed or skipped, also adds a line to ~/.local/state/hafrix/restore_journal.tsv on the server: when it started, where the snapshot came from, which snapshot, the result and the duration. That journal is the measured restore time, not an estimate. Knowing that files leave is not the same as knowing that they come back, and only this proves the second.
If it fails: the run is red in Telegram and names the step. A failure to decrypt means BACKUP_ENCRYPTION_KEY on this host is not the one the snapshot was encrypted with — the old key is the only thing that opens it. If offsite is switched off, the run says so and exits quietly instead of going red.
Change your console password
Who: anyone signed into the console.
- Click your email address in the top bar.
- Fill in Current password and New password (twelve characters minimum) and click Save.
Result: Password changed. appears in the dialog, your other sessions are signed out, and you get the Your Hafrix password was changed email.
If it fails: Current password is incorrect. or The new password must differ from the current one. This dialog is the only place you can do it — the product's Account page belongs to a workspace and your token has none.
Fields
New tenant
| Field | Meaning | Required |
|---|---|---|
| Company | The customer's name as it should appear to them | yes |
| Address | The workspace slug: 3–63 characters, not reserved, never changed afterwards | yes |
| Plan | Free-text label for us; nothing depends on it | no |
| Owner email (optional) | An existing Hafrix account to make admin of the new workspace | no |
The time zone is not asked for here and starts at Asia/Riyadh; the customer's own admin changes it in their settings.
New signup link
| Field | Meaning | Required |
|---|---|---|
| Recipient | Where the invitation goes and who will own the workspace — fixed, not editable by them | yes |
| Company | A suggestion for their form; they can overwrite it | no |
| Suggested address | A suggestion for the slug; checked for availability only when they click | no |
| Note | For us. The recipient never sees it | no |
The link expires fourteen days after it is created.
New announcement
| Field | Meaning | Required |
|---|---|---|
| Title | The headline, and the subject line if you send it by email | yes |
| Message | The body: Bold, Italic, Bullet list, Link | yes |
| Level | Info · Important · Critical — how loudly it arrives | yes |
| Audience | All tenants (active at this moment) or Chosen tenants | yes |
| Send by email as well | Also mail every recipient. Off by default | no |
Backups
| Field | Meaning | Required |
|---|---|---|
| Copy backups offsite | The switch. Off until everything below is filled in | yes |
| Provider | Hetzner Object Storage · Cloudflare R2 · Another S3 storage — a label plus the defaults that go with it | yes |
| Endpoint | The provider's S3 address, starting with https:// |
yes |
| Region | The provider's region code; used when the test button signs its request | no |
| Bucket | The bucket the snapshots go into | yes |
| Key prefix | A folder inside the bucket. Empty means old snapshots are never swept — deleting by age across the root of a bucket we may not own alone is not housekeeping | no |
| Access key | The provider's key id | yes |
| Secret key | Write-only. Empty means keep the key already stored | yes, once |
| Keep offsite for | Days before a snapshot is swept from the bucket. At least 14 | yes |
| Encrypt the dump before uploading | On by default. The passphrase lives in deploy/.env, not here |
yes |
New document
| Field | Meaning | Required |
|---|---|---|
| Slug | The page address: lowercase latin and dashes, a slash for a nested topic | yes |
| Section key | The product section this page documents; empty for a cross-cutting topic | no |
What the system tells you
| When | Audit row | Who gets it | |
|---|---|---|---|
| A workspace is created by hand | Tenant created | — | nobody |
A workspace left without an owner gets one — its earliest active admin — from the assign_tenant_owners repair on the server |
Workspace owner assigned | — | nobody |
| A plan or a status is changed | Tenant updated | — | nobody, including the customer |
| A customer opens a new conversation | Care conversation opened | We have your message ({number}) | the person who wrote — and a push in Telegram with the number, the workspace and the author's role, never the subject; you can answer that push by replying to it |
| A conversation is handed to a person | Care conversation escalated | — | a push in Telegram with the reason code and a button that opens the thread; you can answer it by replying to it |
| The day ends | — | — | one push in Telegram: the day's counts and a button that opens Support |
| A tenant card is opened | Support lookup | — | nobody |
| A person is searched for on People | Support lookup | — | nobody |
| A signup link is created | Signup link created | You're invited to set up {company} on Hafrix | the recipient |
| A signup link is revoked | Signup link revoked | — | nobody |
| Someone registers through a link | Signup link accepted | Welcome to Hafrix — {company} is ready | the new owner |
| An announcement is published | Announcement published | the announcement itself, subject = its title, if you ticked the box | every active person in every addressed workspace |
| An announcement is revoked | Announcement revoked | — | nobody; the rows just go |
| A platform account is created or promoted | Platform admin created | — | nobody |
| You change your console password | — | Your Hafrix password was changed | you; your other sessions are closed |
| You sign in from a device you have not used | — | New sign-in to Hafrix from {device} | you |
| A manual page is published or rolled back | — | — | nobody, deliberately |
| The offsite target is changed | Offsite backup settings changed | — | nobody |
| A snapshot reaches the provider | Backup copied offsite | — | nobody; it is the nightly normal |
| A snapshot does not reach the provider | Offsite backup failed | Offsite backup failed | every Hafrix staff account — and a 🟠 push in Telegram from the server itself |
| Failed sign-ins or 2FA codes, 429s on the sign-in doors or id guessing cross a threshold, or the breached-password check (Pwned Passwords) stays offline for 30 minutes | Abuse alert | Abuse alert: {signal title} | every Hafrix staff account — and a push in Telegram with the signal, count and threshold only, never who or what was hit |
The full catalogue of what Hafrix sends and when is in Emails; how the three layers of in-app notice differ is in Notification codes.
Related
- Roles & access — the other axis entirely: what a role and a group grant open inside a customer's workspace.
- Account — password, second factor and devices as a person sees them.
- Emails — every message Hafrix sends, and what triggers it.
- Notification codes — toasts, the feed and the centred window, and which is which.
- Glossary — the words this manual uses without explaining them again.
FAQ
Why can't I open a customer's items to see what they mean? — Because the console has no screen that could, and the tokens it issues carry no workspace. Ask them for a screenshot, or ask their admin to invite you as a viewer — which is an ordinary membership in an ordinary workspace and looks like one in their own audit. Signing in as somebody else is not built and is not coming without its own decision and its own log.
Where do I approve a company that asked to join? — Nowhere: nobody can ask. Registration is only ever started by a link we send. If a visitor wants an account, they write to us and we send them one.
I revoked a signup link, will the recipient be told? — No. Neither revoking a link nor suspending a workspace sends anything. If the person needs to know, tell them yourself.
The People count on an announcement says 0. — It fills in as the fan-out finishes, one workspace at a time. Reload after a moment.
Why is Publish greyed out on a page I just wrote? — One of two reasons: something is listed under Fix before publishing, or the draft has edits you have not saved yet. Save draft first, then look at the list.
Why can't the agent that wrote the chapter publish it? — By design. Editing and publishing are separate rights, and the server refuses a publish that arrives on a writer token even when the token's owner holds the right in person. Someone has to read the page before the world does.
I changed the bucket — is anything moved? — No. The old snapshots stay where they were and age out under the old bucket's rules; the new bucket starts filling from the next nightly run. If you are switching providers, keep the old bucket until a rehearsal has restored from the new one.
Can I download a snapshot from this screen? — No, and it is not an oversight. A snapshot is the whole customer base; getting one is a deliberate act with a shell, a key and a reason, not a button on a web page.
We rotated the encryption passphrase. What about the copies already uploaded? — They still need the old passphrase, and nothing on our side can change that. Keep the old key for as long as snapshots encrypted with it are still in the bucket — that is what the retention number tells you.
Who can give me the manual rights? — Anyone with a shell on the server: manage.py manual_access --email you@hafrix.com --edit. Like the staff flag itself, it deliberately does not exist as a button in the console — otherwise the person who writes would be the person who approves.
Updated 2026-09-28
New to Hafrix?
See this workflow in your operation
Explore the platform behind these guides: equipment inventory, rentals, maintenance and operational finance. Tell us which workflow you want to see.