Security
How Fixmo protects a repair shop, with the mechanism named
Every claim on this page names the mechanism behind it. Passwords are bcrypt at cost 12, device passcodes are AES-256-GCM, two-factor is open to every role, and the response headers are printed below so you can check them from a terminal yourself.
Getting in
A password is stored as a bcrypt hash at cost factor 12. A hash does not turn back into a password, so nobody at Fixmo can read yours and a stolen copy of the database does not hand anyone a working login. Change it, from settings or through a reset link, and an email goes to the account address saying so.
The session is a signed token in a cookie the browser will not hand to JavaScript, will only send over HTTPS, and will not attach to a request started by another site. It lasts seven days. When it is reissued it keeps the original expiry, so a session that is nearly over never quietly extends itself.
Any account can add a second step, whatever its role and whatever the shop is paying. It is a six digit code from an authenticator app, and once it is on there is nothing to skip: the password alone will not open that account again. Ten single-use recovery codes are issued at setup, for the day a phone goes missing.
A reset link is good for one hour and works once. It refuses a password that matches the current one. An email verification link lasts 24 hours, and a code for changing your email address lasts 15 minutes.
The limits on guessing
Counted in a sliding window rather than a fixed one, so five attempts at 14:59 and five more at 15:00 are ten attempts, not two clean batches of five. Both the address being attacked and the machine doing the attacking are counted, which is what stops one from being used to hide the other.
| Sign in, per IP address | 10 in 15 minutes |
|---|---|
| Sign in, per email address | 5 in 30 minutes |
| Register, per IP address | 5 in an hour |
| Forgot password, per IP address | 10 in an hour |
| Forgot password, per email address | 3 in an hour |
| Reset password, per reset token | 5 in 15 minutes |
| Resend verification, per email address | 3 in an hour |
| Two-factor code, per device | 20 in 15 minutes |
| Two-factor code, per account | 5 in 15 minutes |
The API will not start without them
What someone can see once they are in
The session token proves who someone is. It carries no authority of its own. Whether the account is still active, what role it holds and which branch it belongs to are read from the database on every single request. Take someone off the team and their next request fails, on every device they are signed in on, with nothing to wait for.
Access is 177 separate permissions across 19 modules. The four roles are starting positions rather than fixed boxes, and any one key can be turned off for one person: see cost price, see a customer's phone number, approve a refund they raised themselves. A revoked key is applied on the way out as well as on the way in, so a list endpoint cannot return a field the detail page would have hidden.
Which branch someone is scoped to is a separate question from what they may do, and it is asked separately. Above both sits a check that has nothing to do with permissions: every id arriving in a request body is proved to belong to the caller's shop before it is read or written. Being allowed to raise an invoice is not the same as being allowed to name another shop's customer on it.
What is encrypted, and with what
A repair shop holds the one secret a customer cannot change after the fact: the code that unlocks their phone. That row is the reason this table exists.
| Account passwords | bcrypt at cost factor 12. Hashed, so there is nothing to decrypt and nobody at Fixmo can read one. |
|---|---|
| Override PINs | bcrypt at cost factor 12, the same as a password. A manager's PIN approves a cashier's action on the cashier's own screen without either of them typing a password. |
| Device passcodes, PINs and unlock patterns | AES-256-GCM, a fresh 96-bit random value for every entry, with the authentication tag stored beside the ciphertext. The 32-byte key lives in the server environment and is never in the database. |
| Two-factor secrets and recovery codes | The authenticator secret is AES-256-GCM, the same as the passcodes above. Recovery codes are bcrypt hashes, so the ten a person is handed exist in readable form once and are never stored that way. |
| WhatsApp access tokens and website sync tokens | AES-256-GCM, the same algorithm and the same stored format, so key rotation is one operation rather than three. |
| Uploaded photos and documents | Held in a private bucket. Nothing is publicly readable. Each view is a pre-signed link that stops working after 15 minutes. |
| Everything in transit | HTTPS, with HSTS set to two years and submitted for preload, so a browser refuses plain HTTP before it sends a request. |
The link you send a customer
A repair gets a tracking link with 144 bits of randomness in it. It opens with no login and no app, which is what makes it useful and also what decides what may be on it.
It returns the repair code, the item, the reported fault, the stage the job is at, the estimated date and your shop's own contact details. It does not return the customer's name, their phone number, their email address, any price, or any of their other repairs. The repair's internal id also opens the same page, which is a deliberate fallback for links that were printed before the tokens existed.
Headers, and how to check them
These are the exact strings fixmo.app returns. Run this, and compare the response line by line with the table under it.
curl -sI https://fixmo.app| Strict-Transport-Security | max-age=63072000; includeSubDomains; preload |
|---|---|
| X-Frame-Options | DENY |
| X-Content-Type-Options | nosniff |
| Referrer-Policy | strict-origin-when-cross-origin |
| Permissions-Policy | camera=(self), microphone=(), geolocation=() |
| X-DNS-Prefetch-Control | off |
The content security policy
The page may not be framed by another site, may not have its base address rewritten, and may only post a form back to Fixmo or to PayHere's checkout. Everything else it loads has to come from Fixmo's own origin.
| default-src | 'self' |
|---|---|
| frame-ancestors | 'none' |
| base-uri | 'self' |
| form-action | 'self', and PayHere's checkout |
| font-src | 'self' |
The API accepts browser requests from one origin and no others. Traffic reaches both the site and the API through Cloudflare, so fixmo.app is not answering from an address an attacker can reach directly.
Decisions
The choices behind it, and what each one costs
Security is a set of trades, not a list of features. These are the five that shape how Fixmo behaves day to day, with the price of each one stated rather than left out.
Two-factor is open to every role, on every plan
Admin, Manager, Technician and Cashier can each add an authenticator code to their own sign-in, and a Free shop can do it as readily as a Business one. A technician's login reaches customer records and device passcodes, so gating the protection behind a price would protect the wrong people.
Once it is on, there is no skip
An enrolled account is asked for a code at every sign-in and the password alone will not open it. Ten single-use recovery codes are handed over at setup for the day a phone is lost, and turning two-factor off again needs the password and a live code together.
A session lasts seven days, and can be ended sooner from anywhere
Signing out everywhere, changing a password, or turning two-factor on or off ends every session on that account, on every device, at once. So does an admin deactivating someone in Team. None of it waits for a token to expire.
A tracking link carries no personal data
It opens with no login, so it is written on the assumption that whoever holds it will read it. It returns the repair code, the item, the reported fault, the stage and your shop's contact details. Not the customer's name, phone number, email address or any price.
Card details never reach Fixmo
A subscription is paid on PayHere's own checkout, so no card number passes through this system and none is stored in it. A card payment recorded at your counter is a note that a card was used, not a card being processed.
Questions about security
Reporting a vulnerability
Write to [email protected] with enough detail to reproduce it, and give us a reasonable window before you publish. You will get a reply from a person. Please do not open a public GitHub issue for a security report, because that publishes the problem to everyone before it is fixed.
The same address is published at fixmo.app/.well-known/security.txt.