This guide documents the tested and working security architecture for applications that need:
Public Guest (No Auth) → GuestAgreementForm (token-based access)
↓
Authenticated User → Base44 Login
↓
Invite Validation Check
├─ NOT invited → Immediate logout to home
├─ Invited as 'user' → Blank page (no access)
├─ Invited as 'super_user' → Full workflow access
└─ Invited as 'admin' → Full system access
File: entities/User.json
{
"name": "User",
"type": "object",
"properties": {
"role": {
"type": "string",
"enum": ["user", "super_user", "admin"],
"default": "user",
"description": "User role level"
}
}
}
Role Definitions:
user — Standard login, sees "Access Restricted" message. No workflow access.super_user — Can draft agreements, manage workflows, send emails.admin — Full system access: user management, security monitoring, email templates.File: entities/PendingInvite.json
{
"name": "PendingInvite",
"type": "object",
"properties": {
"email": {
"type": "string"
},
"role": {
"type": "string",
"enum": ["user", "super_user", "admin"]
},
"sent_at": {
"type": "string",
"format": "date-time"
}
},
"required": ["email", "role"]
}
File: entities/SecurityAlert.json
{
"name": "SecurityAlert",
"type": "object",
"properties": {
"alert_type": {
"type": "string",
"enum": ["suspicious_login", "unauthorized_access", "multiple_failures"]
},
"email": {
"type": "string"
},
"description": {
"type": "string"
},
"severity": {
"type": "string",
"enum": ["info", "warning", "critical"],
"default": "warning"
},
"resolved": {
"type": "boolean",
"default": false
}
},
"required": ["alert_type", "email", "description"]
}
File: entities/LoginAttempt.json
{
"name": "LoginAttempt",
"type": "object",
"properties": {
"email": {
"type": "string"
},
"success": {
"type": "boolean",
"default": false
},
"ip_address": {
"type": "string"
}
},
"required": ["email", "success"]
}
File: lib/useInviteValidation.js
This hook must be added to ALL protected pages. It:
import { useEffect } from "react";
import { base44 } from "@/api/base44Client";
export function useInviteValidation() {
useEffect(() => {
(async () => {
try {
const user = await base44.auth.me();
// Admins and super users always pass
if (user?.role === 'admin' || user?.role === 'super_user') {
return;
}
// Regular users must have a pending invite
if (user?.email) {
const invites = await base44.entities.PendingInvite.filter({
email: user.email.toLowerCase()
});
if (!invites.length) {
// Not invited - log out immediately
await base44.entities.SecurityAlert.create({
alert_type: 'unauthorized_access',
email: user.email,
description: `Uninvited user ${user.email} attempted to access protected page`,
severity: 'warning'
});
base44.auth.logout("/GuestAgreementForm");
}
}
} catch (err) {
console.error("Invite validation failed:", err);
}
})();
}, []);
}
File: layout.jsx - Three-Tier Rendering
The layout enforces the security model at the UI level:
For Standard Users:
if (isStandardUser && !isPublicPage) {
return (
<div className="min-h-screen bg-[#F8F7F4] flex flex-col items-center justify-center">
<h1 className="text-2xl font-semibold text-stone-900">Access Restricted</h1>
<p className="text-sm text-stone-400 mt-2">You don't have access to this area yet.</p>
<button onClick={() => base44.auth.logout("/GuestAgreementForm")}>
Sign Out
</button>
</div>
);
}
For Non-Invited Users:
if (!isAdmin && !isSuperUser) {
return (
<div className="min-h-screen bg-[#F8F7F4] flex flex-col items-center justify-center">
<h1 className="text-2xl font-semibold text-stone-900">Access Denied</h1>
<p className="text-sm text-stone-400 mt-2">You don't have permission to access this portal.</p>
<button onClick={() => base44.auth.logout("/GuestAgreementForm")}>
Sign Out
</button>
</div>
);
}
Key Implementation Detail:
Use base44.auth.logout("/GuestAgreementForm") with a public page redirect. This ensures:
File: components/admin/UserManagement.jsx
The admin invites users and assigns roles:
// Invite user with role
await base44.users.inviteUser(email, role);
// Record the pending invite
await base44.entities.PendingInvite.create({
email: email.toLowerCase(),
role: role,
sent_at: new Date().toISOString()
});
When the invited user signs up:
useInviteValidation checks PendingInvite tableFile: components/admin/SecurityMonitor.jsx
Admins see:
File: components/admin/ApprovalAuthCodeManager.jsx and DeleteAuthCodeManager.jsx
For critical operations (approving agreements, deleting completed records):
AppSettings entityTest 1: Unauthorized Signup
Test 2: Authorized Super User
Test 3: Authorized Admin
Test 4: Standard User
Critical: Logout must use redirect parameter
// CORRECT - clears session and redirects to public page
base44.auth.logout("/GuestAgreementForm");
// INCORRECT - does not properly clear session
base44.auth.logout();
window.location.href = '/';
Critical: useInviteValidation must run on protected pages Add this single line to every admin/super_user page:
import { useInviteValidation } from "@/lib/useInviteValidation";
export default function Dashboard() {
useInviteValidation(); // ← Add this
// ... rest of component
}
Clients completing agreement forms are NOT affected by any of this security system.
The GuestAgreementForm page is a public page — it has:
useInviteValidation hookClients access their form via a secure token-based URL sent to them by email:
https://yourapp.com/GuestAgreementForm?id={agreementId}&token={secureToken}
The token is validated server-side. As long as the URL and token are valid, clients can complete their agreement without any account, login, or invite. This entire security system only applies to internal staff accessing the admin portal.
Staff member can't access portal even though invited:
Staff member stuck in login loop after logout:
base44.auth.logout("/GuestAgreementForm")useInviteValidation hookStaff member sees blank page instead of "Access Denied":
Questions?
This guide is production-tested. Customize logos, colors, and authorized pages as needed. Share across all applications that need invite-only admin access.