UniAuth
Shared authentication database with JWT sessions, bcrypt passwords, email verification and password reset — all wired up out of the box.
What is UniAuth?
UniAuth is a separate PostgreSQL database that stores user credentials (email, hashed password, verified status). Multiple Vesta apps can share the same UniAuth database, giving users a single login across all your services.
It connects via PostgreSQL Foreign Data Wrapper (postgres_fdw),
so your app's main database can query UniAuth tables as if they were local.
Setup
Add a [UNIAUTH] section to server.ini:
[UNIAUTH]
DB_NAME = uniauth
DB_HOST = localhost
DB_PORT = 5432
The UniAuth database must be created and initialised separately (it's shared infrastructure).
Your app inherits from Server — UniAuth endpoints are automatically
exposed by the base class.
Auto-exposed routes
When using Server, these routes exist without any code in your app:
| Route | Description |
|---|---|
GET /auth | Serve the login/register page (static/auth.html) |
POST /login | Login or register a user |
GET /logout | Delete JWT cookies and redirect to /auth |
GET /verif | Serve the email verification page |
POST /signup | Verify email with OTP code |
POST /resendVerif | Resend the OTP email |
GET /reset | Serve the password reset page |
POST /passwordReset | Request a reset email |
POST /changePasswordVerif | Reset password with OTP code |
POST /goodbye | Permanently delete the account |
Login & register flow
The /login endpoint handles both new and returning users with the
same call. The frontend sends email + password (and optionally a referral code):
// Frontend — same request for login and register
fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, parrain: referralCode }),
})
The server responds with a string:
| Response | Meaning |
|---|---|
"ok" | Existing account, correct password — JWT set, redirected to DEFAULT_ENDPOINT |
"verif" | New account created — OTP email sent, redirected to /verif |
"invalid email or password" | Account exists but password is wrong |
After a successful login, the server:
- Creates a JWT signed with
SECRET_KEY, valid for 24 hours. - Sets two cookies:
JWT(HttpOnly) andauth(readable by JS). - Calls
self.onLogin(uid)— override this hook in your app. - Redirects to
DEFAULT_ENDPOINTfromserver.ini.
onLogin hook
Override onLogin to run logic the first time a user logs in —
for example to create their profile row in your database.
class App(Server):
def onLogin(self, uid):
# uid is the UniAuth user ID (integer)
existing = self.db.getSomething("profiles", uid)
if not existing:
self.db.insertDict("profiles", {
"id": uid,
"created_at": "NOW()",
"plan": "free",
})
Checking authentication
- No JWT / expired → redirect to
/auth - Valid JWT, user not verified → redirect to
/verif - Valid JWT, user verified → return user ID ✓
verif=True inverts the verification check — it is intended for use
on the /verif page itself: it lets unverified users
through but redirects already-verified users away to DEFAULT_ENDPOINT.
Do not use verif=True in API routes.
@Server.expose
def api_dashboard(self):
uid = self.getUser() # standard usage in every protected route
return json.dumps({"uid": uid})
# verif=True is used internally by the /verif and /resendVerif routes —
# it allows access when unverified and redirects away when already verified.
# You should not need it in your own routes.
getUser() but returns the full decoded payload
{"username": uid, "verified": bool, "exp": datetime}
instead of just the ID. Raises on failure.
JWT internals
Tokens are issued with PyJWT using HS256 and expire after
24 hours.
| Cookie | HttpOnly | Contents |
|---|---|---|
JWT |
Yes | Full signed token — only the server reads this. |
auth |
No | The string "true" — readable by JS as a simple logged-in flag. Does not contain the token itself. |
/login.
Useful if you implement a custom SSO flow.
Email verification
New accounts are created in an unverified state. Vesta sends a 6-digit OTP to the user's email immediately after registration.
- User registers → OTP email sent → redirected to
/verif. - User enters the code →
POST /signup?code=123456. - On success → account is marked verified → redirected to
DEFAULT_ENDPOINT.
The OTP email uses mailing/mailVerif.html.
Customise this template to match your brand.
Password reset
- User submits email →
POST /passwordReset?email=… - Vesta emails a 12-character code (valid 1 hour) using
mailing/mailReset.html. - User submits new password + code →
POST /changePasswordVerif?mail=…&code=…&password=… - Password is rehashed with bcrypt and stored. User is redirected to
/auth.
The reset page is served by GET /reset?email=… from static/reset.html.
Account management
Logout
GET /logout deletes both cookies and redirects to /auth.
On the frontend:
import { logout } from '/static/framework/vesta.mjs';
logout(); // POST /logout then redirects
Delete account
POST /goodbye permanently removes the user from UniAuth.
You should handle cleanup of app-specific data in onGoodbye if needed.
import { goodbye } from '/static/framework/vesta.mjs';
goodbye(); // asks for confirmation, then POSTs /goodbye
Low-level UniAuth DB methods
These are called internally. You can use them directly for custom auth flows.
password is the plaintext password —
it is hashed with bcrypt before storage. parrain is an optional
referral code.
Referral codes
Pass a parrain query parameter to /auth and it will be
forwarded to /login automatically:
<a href="/auth?parrain=INVITE42">Join with invite</a>
The code is passed to db.createAccount(email, password, parrain)
and stored in UniAuth. Handle rewards or access-gating in onLogin.