Authentication
Laravel holds the accounts and decides who gets in. The sign-in and account screens are Flare’s, unchanged, and talk to Laravel through the web app. A signed-in browser keeps a Sanctum token in an httpOnly cookie: the browser sends it, and scripts on the page cannot read it.
What is included
Section titled “What is included”| Password | Sign in, forgot password, reset by an emailed link, change it from the account page. |
| Two-factor | A code from an authenticator app, or a code by email, after the password. Ten backup codes. |
| Passkeys | Face ID, Touch ID, Windows Hello or a security key. No password to type. |
| Emailed link, emailed code | Sign in from an email, without a password. |
| Email verification | A six-digit code, and a link that carries it. |
| Profile | Name, and a picture that is optimised like any image. |
| Devices | Every browser signed in to the account, and signing them out. |
| Sign-up | Off until you switch it on. See below. |
Not included yet: signing in with Google, GitHub and other providers.
The pages
Section titled “The pages”| Page | What it is |
|---|---|
/sign-in |
Email first, then a password, a link or a code. A passkey button beside it. |
/two-factor |
The second step, when the account has one. |
/sign-up |
Create an account. Closed unless registration is on. |
/forgot-password, /reset-password |
Ask for a reset link, and choose the new password. |
/verify-email |
Enter the code from the email, or arrive by its link. |
/dashboard/account |
Profile: picture, name, email. |
/dashboard/account/password |
Change the password. |
/dashboard/account/security |
Two-factor and passkeys. |
/dashboard/account/sessions |
Devices. |
Creating users
Section titled “Creating users”nevela userIt asks for a name, an email and a password. Pass them as options to skip the questions: nevela user --name="Ada Okafor" --email=ada@example.com --password="…".
Letting people sign up
Section titled “Letting people sign up”Sign-up is off by default, and this is deliberate. A generated policy lets every signed-in user do everything until you tighten it. With sign-up open on an untouched app, anyone who found the address could create an account and change your data.
To open it:
- Tighten the policies in
apps/api/app/Policies. - Add
NEVELA_REGISTRATION=truetoapps/api/.env. - Run
nevela generate, so the sign-in page shows the link.
To have new accounts prove their address before they can sign in, set require_email_verification to true in config/nevela.php.
Email in development
Section titled “Email in development”Codes and links are sent with whatever mailer your Laravel app is configured with. A new app uses the log mailer, which sends nothing. So in development Nevela also prints each code or link in the terminal where nevela dev is running:
api │ Nevela mail to ada@example.com: Your Laravel sign-in codeapi │ 482913Copy it from there. In production, set MAIL_MAILER and its settings in apps/api/.env as for any Laravel app.
Two-factor
Section titled “Two-factor”From Account → Security, a person can add a second step to password sign-in.
- An authenticator app. They scan a QR code, then type a code from the app to confirm it. Nothing is switched on until that code is right, so a failed setup cannot lock anyone out.
- Email codes. A six-digit code is emailed at each sign-in.
- Backup codes. Ten are shown once, when two-factor is set up. Each signs in one time when the phone is not to hand. They can be replaced, which stops the old ones working.
Turning two-factor on, off, or replacing the backup codes asks for the password again.
When the second step is asked for:
| Signed in with | Account has an authenticator app | Account’s second step is email only |
|---|---|---|
| a password | asked: the app, an emailed code or a backup code | asked: an emailed code or a backup code |
| an emailed link or code | asked: the app or a backup code, not another email | signed in |
| a passkey | signed in | signed in |
An emailed link followed by an emailed code would be the same proof twice, so someone who had only got into the mailbox would be let in. That is why email is not offered as the second step after an email. A passkey is not asked for more: it is something the person has, unlocked by something they are or know.
A code from the app works once. Typing the same six digits again, in the same half minute, is refused.
Passkeys
Section titled “Passkeys”A passkey is a key pair. The private half stays with the person: on one device, or synced between their own devices by their password manager (iCloud Keychain, Google Password Manager and the like). It is never sent to your app. Laravel keeps the public half. To sign in, the device signs a random challenge and Laravel checks the signature.
From Account → Security → Add a passkey, the browser asks the device to create one. After that, Sign in with a passkey on the sign-in page signs in with no email and no password. Browsers that support it also offer saved passkeys in the email field.
Passkeys are tied to the address of the dashboard, so two things have to be true:
- The dashboard is on
https, or onlocalhost. Browsers refuse passkeys anywhere else. - Laravel knows the dashboard’s address: set
NEVELA_WEB_URL(see Going to production).
The profile picture
Section titled “The profile picture”Uploading a picture on the profile page sends it through the same optimiser as any image field, with the avatar profile: it is turned the right way up, stripped of its metadata, cropped to a 400×400 square in WebP, and given an 80×80 thumbnail for the menus. A 1.2 MB, 4000×3000 photo came out as 37 KB, with a 4 KB thumbnail.
Change the sizes under uploads.profiles.avatar in config/nevela.php.
Devices
Section titled “Devices”Each sign-in creates a token, and each token is a device in Account → Devices, with its browser, its address and when it was last used. The browser and address are what the dashboard reports about the visitor, which Laravel believes because the two share a secret (NEVELA_PROXY_SECRET, written into both apps when the app is created). Someone calling the API directly cannot choose how their device is listed. The page can sign out every other device. Changing the password does the same, and resetting a forgotten password signs out every device, this one included.
Switching methods on and off
Section titled “Switching methods on and off”The choices are in the auth section of config/nevela.php in the Laravel app. Publish the file to change them:
nevela artisan vendor:publish --tag=nevela-config| Key | Default | |
|---|---|---|
registration |
false |
People can create their own account. |
magic_link |
true |
Sign in with an emailed link. |
email_code |
true |
Sign in with an emailed code. |
passkeys |
true |
Passkeys. |
two_factor.authenticator |
true |
An authenticator app as the second step. |
two_factor.email |
true |
Emailed codes as the second step. |
require_email_verification |
false |
Refuse password sign-in until the address is verified. |
check_breached_passwords |
true |
Refuse a new password that appears in a known breach. |
Then run nevela generate. That writes the choices to apps/web/lib/auth-config.ts, so the screens stop offering what is off. Laravel refuses a method that is off whatever the screens show.
Passwords
Section titled “Passwords”A new password needs eight characters. The forms show a strength reading and advice as it is typed, which is advice only: length matters more than punctuation.
A new password is also checked against Have I Been Pwned’s list of breached passwords, and refused if it is on it. Only the first five characters of the password’s hash leave your server, and if the service cannot be reached the password is accepted.
What protects sign-in
Section titled “What protects sign-in”- Attempts are limited to ten a minute per account, so a password cannot be guessed at speed.
- A code allows five wrong guesses, then it is thrown away. Six codes an hour can be sent to one address.
- Links and codes work once and expire: sign-in links and codes in 5 minutes, reset and verification in an hour.
- Nothing says whether an address has an account. Asking for a reset link, a sign-in link or a code gets the same answer either way, and a wrong password and an unknown address get the same answer in the same time.
- Secrets are not readable in the database. Authenticator secrets are encrypted with the app key, backup codes are stored as hashes, and a passkey’s stored half is public by design.
- Emails only link to your dashboard. The address in a link is the one you configured, never one a request supplied. Where to go after signing in is a path on the dashboard and nothing else.
Going to production
Section titled “Going to production”In apps/api/.env:
# Where the dashboard is. Links in emails point here, and passkeys are tied to it.NEVELA_WEB_URL=https://app.example.com
# A real mailer, as for any Laravel app.MAIL_MAILER=smtpMAIL_FROM_ADDRESS=hello@example.com
# The same value as in the dashboard's environment. See Devices, above.NEVELA_PROXY_SECRET=a-long-random-stringAnd NEVELA_PROXY_SECRET with the same value wherever the dashboard runs. An app created with 0.4.0 or later has one in both .env files already; copy it to your hosts. Without it, a device is listed with the address the request reached Laravel from, which in production is your dashboard’s server.
In development you do not need NEVELA_WEB_URL: any localhost address is accepted, because the dashboard moves to another port when 3000 is taken.
Over the API
Section titled “Over the API”Every endpoint is under /api/auth. The REST API reference lists them. In short, to get a token:
curl -X POST http://127.0.0.1:8000/api/auth/token \ -H 'Accept: application/json' -H 'Content-Type: application/json' \ -d '{"email": "ada@example.com", "password": "…"}'{ "token": "1|…", "user": { "id": "1", "name": "Ada Okafor", "email": "ada@example.com" } }Send it as Authorization: Bearer <token> on every other request. If the account has two-factor on, there is no token in that answer yet: see the reference.
How the browser stays signed in
Section titled “How the browser stays signed in”Scripts on the page never see the token. The auth screens call the web app’s own /api/auth/… route, which passes each request to Laravel. When Laravel answers with a token, the route puts it in an httpOnly cookie named nevela_token, which the browser stores and sends but page scripts cannot read, and removes it from the answer. It lasts 30 days. Signing out revokes the token in Laravel and removes the cookie.
lib/auth-client.ts in the web app has the browser’s side of this, under the method names Flare’s screens call. That is why the screens could be copied without changes.
Using your own auth
Section titled “Using your own auth”Set auth.enabled to false in config/nevela.php and Nevela registers none of these routes. Provide your own auth:sanctum tokens, or change middleware to the guard you use.