On May 26, 2026, a new company signed up for KashBoss using an email address and a password. That should have been impossible. Three weeks earlier I had removed email signup completely, and our registration screen showed one button only: continue with Google. I asked my AI assistant how this person had signed up with email.
The answer was uncomfortable. We had removed the email form from the screen, and only from the screen. The signup code still existed inside the app, and the email signup switch in our authentication service was still on. Anyone with an older saved copy of the web app, or anyone calling the login service directly, could still create an account. The lock existed only where it was visible.
Logins feel like a screen problem: a form, a button, an error message. They are really a server problem.
The short answer
Every login rule that matters, such as who can sign up, who can log in and how many password guesses are allowed, must be enforced on the server, because the screen is only one of the ways in. Hiding a signup form does not disable signup. Turn the feature off in your authentication settings, delete the unused code, and limit wrong password attempts where a direct caller cannot skip the limit.
Login basics in plain words
Authentication means proving who you are. Authorization means deciding what you may do once in. This article covers the first.
There are three common ways to log in: email and password, "Sign in with Google" (Google confirms the person and you never store their password), and magic links or passkeys, which avoid passwords entirely. Passwords are never stored as typed. The system stores a hash, a one-way fingerprint, and compares fingerprints at login. A login service such as Supabase Auth does this for you, and you should let it.
After login, the app receives an access token, a short-lived pass valid for about an hour, and a refresh token, a longer-lived ticket for getting a new pass. Picture a festival wristband that expires every hour, and a ticket in your pocket to collect a new one.
The screen is a suggestion. The server is the law.
How we did it in KashBoss
Disable on the server, not on the screen
We "disabled" email signup by removing the form from the screen. The signup functions were still in the app's code, and new email signups were still allowed in our authentication settings. A real company signed up by email weeks after I believed it was impossible.
The fix was to turn off new email signups in the authentication settings (keeping email login for existing users) and delete the unused signup code, so nobody can re-expose it by accident. Now, whenever someone says "that feature is off," we ask one question: off on the screen, or off on the server? We chose Google-only signup in the first place because a mistyped email is worse than it looks. That customer can never receive a password reset and cannot be contacted. Google gives verified addresses and faster signup.
Employee accounts have their own rules
In business software, employees usually do not sign up. The owner creates each account with a name, email, first password and role, and the employee must change that password at first login. Each rule came with a bug:
- Emails must be unique across owners and employees. One owner used his own email for an employee, and the function that decides a person's role found the employee first and treated the owner as a warehouse keeper.
- Compare emails without capital letters. A sales rep could not log in because his phone keyboard capitalized the first letter of his email and the comparison was case-sensitive. We fixed it on the server, so old app versions benefited immediately.
- Test the normal path after every security change. When we tightened the "change password" function, we blocked new employees from changing their temporary password.
The biggest lesson was this one.
Employees used to log in before they had any session. To check the password, the app read the employees table directly as an anonymous visitor and compared the fingerprint on the device. For that to work, a database rule let anonymous visitors read the employees table. Anyone with the app's public key could read every employee of every company: names, emails, phone numbers and password fingerprints. An external review found it on June 1, 2026.
We moved the password check into a server function that returns the employee's details without the fingerprint. We released that first, confirmed employees could still log in, and only then removed the open rule. Build the new door, test it, then close the old one. Later we added a guessing limit on the server, since the app's limit on the device did not stop anyone calling the server directly.
Never log people out by surprise
In August 2026 I gave a simple instruction: the app must never log anyone out unless they press the logout button. Being thrown out in the middle of work is especially painful for a sales rep standing in a customer's shop. We found six separate paths that logged people out without their choice. A network failure while loading user details sent them to the login screen even though a saved copy sat on the device, and the login library announced "signed out" after one failed renewal.
One case was strange. A customer was logged out every time he imported a product list. His computer clock ran about an hour ahead, so the login library believed his pass was always about to expire and renewed it every ten seconds. Each renewal cancels the previous pass, and during an import some requests carried a canceled pass, so the server refused them and the library logged him out. The clue: his phone worked, his laptop did not.
Treat "could not check" differently from "no." A deleted employee used to keep working on his device because "the server could not be reached" and "this employee no longer exists" came back as the same empty answer. Now the server answers with one of three results: allowed, blocked, or try again.
Automatic messages
Password resets, welcome messages and receipts must arrive once and say the right thing. For each message, store one record per event and create it before sending, so a job that runs twice finds the record and does nothing. And "delivered" does not mean "correct": we once sent an email whose whole body read "[object Object]" while the sending service reported success. After a new automatic message goes live, open a real copy and read it. Chapter 11 of the book also covers Google sign-in for employees and the full session story.
Key takeaways
- A lock on the screen is not a lock. Signup, login and guessing limits belong on the server.
- Let a trusted provider handle owner passwords and verification.
- Employee accounts need their own rules: created by the owner, unique emails, case-insensitive matching, forced first password change.
- End sessions only when the user decides, and treat "could not check" differently from "no."
- Send each automatic message once per event, and read the delivered copy.
Frequently asked questions
How do I disable signup in Supabase properly?
Turn off new signups in the authentication settings, not only on the screen, and remove the unused signup code. Keep login enabled for existing users if you need it.
Why should password attempt limits be on the server?
A limit inside the app only stops people using the app. Anyone can call the login service directly, so without a server-side limit they can guess passwords without restriction.
What is a refresh token?
It is a longer-lived ticket the app uses to get a new short-lived access token before the old one expires. It keeps users logged in without asking for the password again every hour.