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.

12bcrypt cost factorPasswords and PINs
256Bit key on stored passcodesAES-256-GCM
177Separate permissionsAcross 19 modules
15Minute life of an upload linkPre-signed, then dead

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 address10 in 15 minutes
Sign in, per email address5 in 30 minutes
Register, per IP address5 in an hour
Forgot password, per IP address10 in an hour
Forgot password, per email address3 in an hour
Reset password, per reset token5 in 15 minutes
Resend verification, per email address3 in an hour
Two-factor code, per device20 in 15 minutes
Two-factor code, per account5 in 15 minutes

The API will not start without them

In production the server refuses to boot if the rate limiter is not configured, or if the token signing secret is missing, shorter than 32 characters, or one of the placeholder values a setup guide leaves behind. A misconfiguration that would quietly remove a protection stops the deploy instead.

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 passwordsbcrypt at cost factor 12. Hashed, so there is nothing to decrypt and nobody at Fixmo can read one.
Override PINsbcrypt 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 patternsAES-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 codesThe 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 tokensAES-256-GCM, the same algorithm and the same stored format, so key rotation is one operation rather than three.
Uploaded photos and documentsHeld in a private bucket. Nothing is publicly readable. Each view is a pre-signed link that stops working after 15 minutes.
Everything in transitHTTPS, 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-Securitymax-age=63072000; includeSubDomains; preload
X-Frame-OptionsDENY
X-Content-Type-Optionsnosniff
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(self), microphone=(), geolocation=()
X-DNS-Prefetch-Controloff

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

Yes, on any account and any plan. Each person turns it on for themselves from their profile page, using any app that makes six digit codes: Google Authenticator, Authy, 1Password and the rest. Once it is on, that account is asked for a code at every sign-in and there is no way to skip it. Ten single-use recovery codes are issued at setup for a lost phone.

One button on your profile page ends every session on your account, on every device, immediately. Changing your password does the same thing, and so does turning two-factor on or off. An admin deactivating someone in Team ends that person's sessions the same way, without them having to do anything.

Seven days, and the clock does not restart on its own. A session that is reissued in the middle of its life keeps the expiry it was minted with, so it never quietly extends. It can end sooner: a password change, a sign-out everywhere, a two-factor change or a deactivation all end it immediately.

As bcrypt hashes at cost factor 12. A hash cannot be turned back into a password, so nobody at Fixmo can read yours, and a database copy does not hand an attacker a usable password. Changing a password sends an email to the account address whether it was changed from settings or through a reset link.

Their next request fails, on every device, without waiting for anything to expire. The session token proves who someone is and nothing else. Whether the account is still active, what role it holds, which branch it belongs to, and whether the session has been ended are all read from the database on every single request.

No. Every record is scoped to a shop, and every id that arrives in a request body is proved to belong to the caller's shop before it is read or written. That check is separate from the permission check, because a person can legitimately be allowed to create an invoice and still have no business naming another shop's customer on it.

Yes. Device passwords, PINs and unlock patterns are encrypted with AES-256-GCM, each entry with its own random initialisation value and its own authentication tag. The key is held in the server environment and is never written to the database, so a copy of the database on its own does not decrypt them.

Anyone holding the link, which is why it carries no personal data. The link uses a 144-bit random token, and it returns the repair code, the item, the reported fault, the current stage, the estimated date and your shop's contact details. It does not return the customer's name, phone number, email address or any price.

No, and neither has been sought yet. They are audits aimed at enterprise procurement, and Fixmo has put the same effort into the things a repair shop feels directly: two-factor on every role, sessions that can be ended everywhere at once, encrypted device passcodes, and authorization resolved from the database on every request. Card details never reach Fixmo, because a subscription is paid on PayHere's own checkout.

Run curl -sI https://fixmo.app and read the response headers. Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy and Content-Security-Policy are all on it, with the exact values printed on this page.

Email [email protected] with enough detail to reproduce it, and give us a reasonable window before publishing. The same address is published at https://fixmo.app/.well-known/security.txt. Please do not open a public GitHub issue for a security report.

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.

[email protected]

The same address is published at fixmo.app/.well-known/security.txt.