Full specification for centralized user provisioning, tiered access control, and admin panel architecture across the SaludCap website — covering investor portals, gated research documents, employee sections, and the Salud Vault confidential appendix. Prepared for handoff to the website development team.
Today, access to gated Salud Capital content is managed in two disconnected ways: research document HTML files contain hardcoded SHA-256 password hashes and manually maintained email lists, and the website has no centralized user management. This creates operational drag — adding an investor means editing code — and no audit trail of who accessed what.
The target state is a single Salud Capital Admin Panel where any authorized administrator can provision, modify, and revoke access for employees, investors, and research readers across every gated surface of the SaludCap platform — website sections, investor portals, and individual research documents — from one interface.
"Every access decision — who sees investor research, who enters the Vault appendix, who reads the employee playbook — should be provisioned, audited, and revoked from one admin interface. No code edits required."
| Tier | Label | Who | Provisioned By | Auth Method |
|---|---|---|---|---|
| ADMIN | Super Admin | Salud Capital founders, designated ops lead | Manual (database seed) | Email + strong password + TOTP 2FA |
| EMPLOYEE | Team Member | Any @saludcap.com or @saludwireless.com address | Admin panel: domain auto-grant or individual invite | Email + password (SSO-ready) |
| INVESTOR | Investor / Partner | Named individuals provisioned by admin | Admin panel: individual email invite with expiry | Email magic link OR email + password |
| PUBLIC | Public Reader | Anyone with the URL | None — no account required | None |
★ Investors get Vault Appendix access only if the "Vault Appendix" permission toggle is individually enabled for their account in the admin panel. Default = off for investors.
Beyond tier, each user account carries a set of boolean permission flags that admin can toggle independently. This allows granular control — e.g. an investor who gets all research but not the Vault Appendix, or a research partner who gets one specific report but not the investor portal.
| Flag | Key | Default (Investor) | Default (Employee) | Description |
|---|---|---|---|---|
| Investor Portal | can_investor_portal | ✓ On | ✓ On | Access to /investor and all investor portal pages |
| Gated Research | can_gated_research | ✓ On | ✓ On | Access to investor-gated research reports |
| Vault Appendix | can_vault_appendix | ✗ Off | ✓ On | Reveals Salud Vault confidential appendix in research docs |
| Employee Sections | can_employee_sections | ✗ Off | ✓ On | Internal playbooks, build tracker, partner contacts |
| Specific Report Override | report_allowlist[] | — | — | Array of report IDs — grants access to specific reports regardless of tier |
| Account Expiry | expires_at | Set per invite | null (no expiry) | ISO datetime; null = no expiry. Useful for time-boxed investor data room access |
Central auth API · JWT issuance · Session management
Hosted: Vercel Edge Functions or Cloudflare Workers
Postgres (Supabase or PlanetScale)
Users · Roles · Flags · Invite tokens · Audit log
Magic links · Invite emails · Password reset
Recommended: Resend or SendGrid
Browser checks for valid JWT in httpOnly cookie (website) or localStorage key sc_auth_token (standalone HTML research docs). If no token or token expired → redirect to login.
User submits email + password at auth.saludcap.com/login. Service validates credentials against database. On success, issues a signed JWT (RS256) containing: sub (user ID), email, tier, permissions object, exp (24h default, 7d for "remember me").
Website pages: JWT in httpOnly cookie, validated server-side via Vercel middleware. Standalone research HTML files: JWT in localStorage, read client-side via fetch() call to /api/verify-token. Token payload contains all permission flags — no additional DB call needed per page.
Website: Next.js middleware checks JWT claims before rendering protected routes. Research HTML: client-side JS calls /api/verify-token, receives permission object, shows or hides content sections based on flags. can_vault_appendix: true → Vault appendix revealed. can_gated_research: false → redirect to request access page.
Every gated page view, research document open, and Vault appendix unlock writes an event to the audit log table: (user_id, event_type, resource_id, ip_address, user_agent, timestamp). Visible in admin panel. Exportable to CSV.
GET /api/verify-token with the user's JWT. The API returns the full permission object. The document renders based on flags. Zero credentials stored in the HTML file.
// ❌ Old approach — hardcoded in each HTML file
const PASS_HASH = '98364381a28f...';
const AUTHORIZED = ['@saludcap.com', '@saludwireless.com'];
async function vaultAuth() { /* SHA-256 check */ }
// ✅ New approach — research HTML calls auth service
async function initResearchDoc() {
const token = localStorage.getItem('sc_auth_token');
if (!token) { showLoginPrompt(); return; }
const res = await fetch('https://auth.saludcap.com/api/verify-token', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) { showLoginPrompt(); return; }
const { permissions, email } = await res.json();
// permissions = { can_gated_research, can_vault_appendix, ... }
if (permissions.can_vault_appendix) {
document.getElementById('vault-appendix').classList.add('unlocked');
}
logAccess(token, 'digital_payments_research_v1'); // audit event
}
-- Users table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT UNIQUE NOT NULL,
password_hash TEXT, -- bcrypt; null if magic-link-only
tier TEXT NOT NULL DEFAULT 'investor', -- admin | employee | investor | public
display_name TEXT,
company TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
last_login_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ, -- null = no expiry; set for time-boxed investors
is_active BOOLEAN DEFAULT true,
invited_by UUID REFERENCES users(id)
);
-- Permission flags (one row per user, all flags)
CREATE TABLE user_permissions (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
can_investor_portal BOOLEAN DEFAULT false,
can_gated_research BOOLEAN DEFAULT false,
can_vault_appendix BOOLEAN DEFAULT false, -- Salud Vault confidential appendix
can_employee_sections BOOLEAN DEFAULT false,
report_allowlist TEXT[] DEFAULT '{}', -- array of report slugs
updated_at TIMESTAMPTZ DEFAULT now(),
updated_by UUID REFERENCES users(id)
);
-- Authorized email domains (auto-grants EMPLOYEE tier)
CREATE TABLE authorized_domains (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
domain TEXT UNIQUE NOT NULL, -- e.g. 'saludwireless.com'
auto_tier TEXT DEFAULT 'employee',
added_by UUID REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT now()
);
-- Invite tokens (for investor onboarding)
CREATE TABLE invite_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
token TEXT UNIQUE NOT NULL DEFAULT gen_random_uuid()::text,
email TEXT NOT NULL,
tier TEXT NOT NULL DEFAULT 'investor',
permissions JSONB, -- pre-set permission flags for this invite
expires_at TIMESTAMPTZ NOT NULL, -- invite link expiry (typically 7 days)
used_at TIMESTAMPTZ,
created_by UUID REFERENCES users(id)
);
-- Audit log
CREATE TABLE access_log (
id BIGSERIAL PRIMARY KEY,
user_id UUID REFERENCES users(id),
email TEXT, -- denormalized for easy log reading
event_type TEXT NOT NULL, -- login | view_research | unlock_vault | invite_sent | access_revoked
resource_id TEXT, -- report slug, page path, etc.
ip_address INET,
user_agent TEXT,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Default permissions by tier (for new user initialization)
-- Employee: all internal flags on. Investor: portal + research on; vault off.
CREATE VIEW default_permissions AS
SELECT 'employee' AS tier, true AS can_investor_portal, true AS can_gated_research,
true AS can_vault_appendix, true AS can_employee_sections
UNION ALL
SELECT 'investor', true, true, false, false;
| Component | Recommended | Alternative | Notes |
|---|---|---|---|
| Database | Supabase (Postgres) | PlanetScale, Neon | Supabase has built-in auth, row-level security, and a REST API — reduces custom code significantly |
| Auth Layer | Supabase Auth | NextAuth.js, Clerk | Supports email+password, magic links, and OAuth; custom JWT claims via hooks |
| Framework | Next.js 14+ (App Router) | Remix, Astro | Already in SaludCap tech stack; Vercel-native; middleware for route protection |
| Hosting | Vercel | Cloudflare Pages | Already connected via GitHub → Vercel pipeline |
| Resend | SendGrid, Postmark | Developer-friendly; React Email templates; generous free tier | |
| Admin UI | shadcn/ui + Tailwind | Tremor, Ant Design | Already used in SaludCap artifacts; no additional CSS framework needed |
The admin panel lives at /admin and is accessible only to accounts with tier = 'admin'. It is the single interface for all access management across the SaludCap platform.
| Screen | Route | Key Functions |
|---|---|---|
| Dashboard | /admin |
Active user count by tier; recent access events; expiring accounts (next 7 days); quick-invite button |
| All Users | /admin/users |
Table of all accounts. Columns: email, name, company, tier, expiry, permission toggles (Research, Vault, Employee, Investor Portal), last login, Active toggle. Inline edit all flags. Click row → detail page. |
| User Detail | /admin/users/[id] |
Full permission set; expiry date picker; report allowlist editor; access history for this user; revoke all sessions button; delete account button. |
| Invite New | /admin/users/invite |
Email field; tier dropdown; pre-set permission toggles; expiry date (default 90 days); optional personal note in email. Generates and sends invite link. Shows copyable invite URL. |
| Domains | /admin/domains |
Add/remove authorized email domains. Each domain: domain string, auto-tier (employee/investor), date added, added by. Domain users auto-provisioned on first login with domain-default permissions. |
| Research Docs | /admin/research |
Registry of all gated research reports. Each doc: slug, title, required tier, Vault appendix yes/no, access count, last accessed. Toggle individual reports between public/gated. Add new report to registry. |
| Audit Log | /admin/audit |
Filterable log of all access events: login, view_research, unlock_vault, invite_sent, access_revoked. Filter by user, event type, date range. Export to CSV. |
Enters investor email, selects tier "Investor," sets permission toggles (Research: on, Vault: off by default), sets expiry (e.g. 90 days), adds optional note.
Stores invite record in invite_tokens table. Sends branded email to investor with invite link: auth.saludcap.com/accept-invite?token=xxxx. Admin sees copyable link on screen as backup.
Token validated (not expired, not used). Investor sets password. Account created in users table with pre-configured permissions from invite record. Welcome email sent.
JWT issued on login contains their permission flags. Research documents, investor portal, and Vault appendix (if flag enabled) all respond to the same JWT without additional provisioning.
All endpoints hosted under https://auth.saludcap.com/api/ (Vercel Edge Function or Next.js API route). All authenticated endpoints require Authorization: Bearer <jwt>.
| Method | Endpoint | Auth Required | Description |
|---|---|---|---|
POST | /api/auth/login | — | Email + password → returns signed JWT + user object |
POST | /api/auth/magic-link | — | Email → sends magic link email; for password-less investor login |
POST | /api/auth/accept-invite | — | Invite token + new password → creates account, returns JWT |
POST | /api/auth/logout | User | Invalidates session; clears cookie |
GET | /api/verify-token | User | Validates JWT; returns {email, tier, permissions}; used by standalone research HTML files |
POST | /api/log-access | User | Writes event to audit log; called by research docs on open/unlock |
GET | /api/admin/users | Admin | List all users with permissions |
POST | /api/admin/users | Admin | Create user directly (no invite) |
PATCH | /api/admin/users/[id] | Admin | Update tier, permissions, expiry, active status |
DELETE | /api/admin/users/[id] | Admin | Deactivate account; does not hard-delete (preserve audit trail) |
POST | /api/admin/invite | Admin | Generate invite token; send invite email; returns invite URL |
GET | /api/admin/domains | Admin | List authorized domains |
POST | /api/admin/domains | Admin | Add authorized domain + auto-tier |
DELETE | /api/admin/domains/[id] | Admin | Remove authorized domain |
GET | /api/admin/audit | Admin | Paginated audit log with filters |
GET | /api/admin/audit.csv | Admin | CSV export of audit log |
// GET /api/verify-token
// Response 200:
{
"valid": true,
"user_id": "uuid-...",
"email": "investor@example.com",
"tier": "investor",
"display_name": "J. Smith",
"permissions": {
"can_investor_portal": true,
"can_gated_research": true,
"can_vault_appendix": false, // ← controls Vault appendix visibility
"can_employee_sections": false,
"report_allowlist": ["genius_act_research", "crypto_policy_trump_biden"]
},
"expires_at": "2026-07-01T00:00:00Z"
}
// Response 401 (expired or invalid token):
{ "valid": false, "error": "token_expired" }
// Drop-in script for every gated research HTML file
// Replaces the current hardcoded SHA-256 auth block entirely
(async function saludAuth() {
const token = localStorage.getItem('sc_auth_token');
const loginUrl = 'https://saludcap.github.io/salud/login?return='
+ encodeURIComponent(location.href);
// No token → redirect to login
if (!token) { location.href = loginUrl; return; }
try {
const res = await fetch('https://auth.saludcap.com/api/verify-token', {
headers: { Authorization: 'Bearer ' + token }
});
if (!res.ok) { location.href = loginUrl; return; }
const { permissions, email, tier } = await res.json();
// Gate: gated research requires can_gated_research
if (!permissions.can_gated_research) {
document.getElementById('gated-gate').style.display = 'block';
return;
}
// Vault Appendix: controlled by can_vault_appendix flag
if (permissions.can_vault_appendix) {
const vault = document.getElementById('vault-appendix');
if (vault) vault.classList.add('unlocked');
}
// Show user identity in nav
document.getElementById('user-email').textContent = email;
// Log access event
fetch('https://auth.saludcap.com/api/log-access', {
method: 'POST',
headers: { Authorization: 'Bearer ' + token,
'Content-Type': 'application/json' },
body: JSON.stringify({
event_type: 'view_research',
resource_id: document.documentElement.dataset.reportSlug
})
});
} catch (e) {
console.error('Auth check failed', e);
// Fail open on network error for offline access — or fail closed:
// location.href = loginUrl;
}
})();
Every gated research document should be registered in the admin panel's Research Docs screen. This allows access rules to be changed without touching the HTML files, and gives the audit log meaningful resource identifiers.
| Report Slug | Title | Current Gate | Vault Appendix | Min Tier |
|---|---|---|---|---|
digital_payments_v1 | Digital Payments, Tokens, Smart Contracts & Secure Identity | Investor | ✓ Yes | Investor |
genius_act_research | The GENIUS Act: America's First Crypto Law | Public | — No | Public |
crypto_policy_trump_biden | Trump vs. Biden: Digital Asset Policy Comparison | Public | — No | Public |
defi_ux_senior_unbanked | DeFi UX: Simplicity for Seniors & Underbanked | Investor | — No | Investor |
data-report-slug="digital_payments_v1" to the <html> tag of each research document. The auth script reads this to log the correct resource ID. The admin panel can then show per-report access stats.
<!-- Add to <html> tag of each research document -->
<html lang="en"
data-report-slug="digital_payments_v1"
data-report-title="Digital Payments, Tokens, Smart Contracts & Secure Identity"
data-vault-appendix="true">
| Requirement | Implementation | Priority |
|---|---|---|
| Password hashing | bcrypt with work factor ≥ 12. Never store plaintext or MD5/SHA-256 of passwords. | Critical |
| JWT signing | RS256 (asymmetric). Private key in Vercel/Supabase secrets. Public key for verification only. | Critical |
| HTTPS only | All auth API endpoints enforce HTTPS. HSTS header required. | Critical |
| Rate limiting | Login endpoint: max 5 attempts per 15 min per IP. Magic link: 3 per hour per email. | Critical |
| TOTP 2FA for admins | Admin accounts require TOTP (Google Authenticator / Authy). TOTP secret stored encrypted in DB. | Critical |
| Invite token expiry | Invite links expire after 7 days. Single-use only. Mark used_at on first use. | High |
| Session revocation | Maintain a JWT revocation list (Redis or DB) for forced logout. Check on every verify-token call. | High |
| CORS policy | Auth API allows: saludcap.github.io, *.saludcap.com, localhost:* (dev only). | High |
| Audit log immutability | Audit log rows are insert-only. No update or delete permissions on access_log table. | High |
| Account expiry enforcement | JWT issuance checks expires_at. Expired accounts return 401 on verify-token. Nightly job deactivates expired accounts. | Medium |
| Soft delete only | Never hard-delete users. Set is_active = false. Preserves audit trail. | Medium |
| Research HTML offline risk | Standalone HTML files can be saved locally. Auth gate is client-side. For truly sensitive content: serve from authenticated Next.js routes instead of standalone HTML. | Note |
saludcap.com and saludwireless.com into authorized_domains table/login — email + password form, magic link option. Branded with SaludCap design tokens. Returns JWT to localStorage on success./admin protected route (admin tier only). Sidebar navigation. Dashboard with summary stats.salud-auth.js on Vercel (or GitHub Pages). All research docs load it via <script> tag instead of inline JS.data-report-slug attribute. Add <script src="salud-auth.js"></script>./login?return=[doc-url] → after login, redirect back to doc. JWT stored in localStorage, persists across tabs.permissions.can_vault_appendix; reveals #vault-appendix div if true. Remove current client-side gate.access_log and appears in admin audit screen.can_investor_portal on all /investor/* routes.can_employee_sections on internal pages.SaludVault2026!@saludcap.com, @saludwireless.comconst AUTHORIZED array in the HTML file's <script> block.saludcap.github.io/salud/ pages tree until the auth service is live.
| File | Location | Gate Type | Migration Priority |
|---|---|---|---|
salud_digital_payments_research.html | Outputs / Vercel deploy | SHA-256 inline JS (Vault appendix only) | Phase 3 — High |
salud_digital_payments_research_PUBLIC.html | Outputs / Vercel deploy | No gate — locked notice only | Replace with gated version post-auth |
genius_act_research.html | Project / GitHub | None — public | No action required |
crypto_policy_trump_biden.html | Project / GitHub | None — public | No action required |
| Topic | Resource |
|---|---|
| Auth framework | Supabase Auth Docs — email+password, magic links, JWT customization |
| Next.js route protection | Next.js Authentication Guide — App Router, middleware, session management |
| JWT best practices | Auth0 — JWT Best Current Practices |
| Row Level Security | Supabase RLS Guide — protect DB rows by user role |
| Email (magic links) | Resend Docs + React Email for branded invite templates |
| Rate limiting | Upstash Rate Limit — Redis-backed, Vercel Edge compatible |
| TOTP 2FA | otpauth (npm) — TOTP generation and verification in Node.js |
| Admin UI components | shadcn/ui Blocks — pre-built dashboard, table, and form layouts |
| Current SaludCap stack | GitHub: saludcap.github.io/salud/ → Vercel deployment pipeline. Local dev path: C:\Users\Admin\Documents\ai coding\salud |