Email Auth API

Ship Auth in
20 Minutes

Email verification → signed JWT → done. No user database. No vendor lock-in. Just the one primitive you actually need.

Free up to 50 MAU

What This Isn't

Not a user database
Not SSO or social login
Not a compliance checkbox
Not a thing you're stuck with

We do exactly one thing: verify an email and sign a JWT. Your public key, your RLS policies, your data. Outgrow us? Swap in any JWT issuer. Your Postgres policies don't change.

Who This Is For

🚀

Vibe Coders

Ship your side project this weekend. Auth shouldn't be the hard part.

💡

Startups

Stop paying for features you'll never use. Start with this, migrate if you need more.

🏢

Enterprises

POC in a day. Your security team can audit two endpoints.

🔐

Eliminate Password Headaches Forever

Passwords are the #1 attack vector in security breaches. Every password you store is a liability waiting to happen. With Effortless Magic Links, you never store a password again.

No Password Storage

Nothing to hash, nothing to leak. Users prove identity through email verification.

No Password Resets

Eliminate "forgot password" flows and the support tickets that come with them.

No Credential Stuffing

Attackers can't reuse leaked passwords from other sites against your app.

RS256 Signed JWTs

Cryptographically secure tokens that can be verified anywhere without shared secrets.

Up and Running in Minutes

No complex OAuth flows. No third-party dependencies. Just a simple, secure authentication system you can integrate in an afternoon.

1
User Enters Email

They enter their email address. A 6-digit code is instantly sent to their inbox.

2
User Enters Code

They type the 6-digit code from their inbox. No clicking email links, no redirects.

3
You Get a Signed JWT

A signed JWT with the verified email. Use it in your backend or Postgres RLS policies.

Just Two API Endpoints

That's all you need to integrate:

POST /email-auth/request-code
POST /email-auth/redeem-code
🐘

Your Database Enforces Access. Not Your App.

Public key lives in Postgres. auth.email() in your RLS policies. Defense in depth without middleware.

CREATE POLICY user_data ON orders
USING (customer_email = auth.email());

Even if your app has bugs, the database won't leak.

Predictable Pricing. Stupid Margins.

No per-auth fees. No overages. MAU = unique emails per month.

Free
$0
50 MAU
Indie
$49/yr
500 MAU
Pro
$199/yr
5,000 MAU
Business
$999/yr
50,000 MAU
Enterprise
Let's talk
50,000+ MAU
Open source verification examples | Uptime status | We store a hash of your auth events. That's it.
1
Details
2
Keys
3
Register
Create Your Tenant
A tenant represents your application or service that will use magic link authentication

Each tenant gets its own RSA key pair for signing JWTs. Your users will authenticate via email, and you'll receive cryptographically signed tokens to verify in your backend or database.

A friendly name to identify this tenant (e.g., your app or project name)
How long tokens remain valid (300 = 5 minutes)
Auto-generated unique identifier
My Tenant
Theme:
Current Session
🔒
Database Explorer
public
customers 0
orders 0
products 0
SELECT * FROM customers;
0 rows
# namevarchar emailvarchar phonevarchar rolevarchar registered_attimestamp order_countint4
Connected
RLS: enabled
Demo Database
1
Test SQL
2
RLS Policies
3
effortless-postgres
4
Trust Public Key
Install effortless-postgres
JWT verification for Postgres RLS

This script creates the auth schema with all necessary tables and functions for JWT verification and session management.

Run the script using psql:

Terminal
psql -d your_database -f install-effortless-magic-links.sql
The script is idempotent - you can safely run it multiple times to update to the latest version.
What's Inside
The script creates these key components
-- auth schema setup
CREATE SCHEMA auth;

-- store trusted public keys
CREATE TABLE auth.trusted_tenants (
    tenant_id TEXT PRIMARY KEY,
    public_key_pem TEXT NOT NULL,
    ...
);

-- JWT verification function
CREATE FUNCTION auth.set_jwt(token TEXT)
    RETURNS BOOLEAN AS $$
    -- decode & verify JWT signature
    -- extract claims (email, role, etc)
    -- store in session variables
    ...
$$ LANGUAGE plpython3u;

-- helper functions for RLS
CREATE FUNCTION auth.email() RETURNS TEXT;
CREATE FUNCTION auth.role() RETURNS TEXT;
CREATE FUNCTION auth.tenant_id() RETURNS TEXT;
CREATE FUNCTION auth.claim(key) RETURNS TEXT;
Prerequisites

The auth schema uses Python for JWT verification. Your Postgres server needs:

  • plpython3u extension — Run CREATE EXTENSION IF NOT EXISTS plpython3u; as superuser
  • PyJWT package — Run pip install pyjwt on the Postgres server
  • Cryptography package — Run pip install cryptography on the Postgres server
Step 3 of 4

The auth.trusted_tenants table stores public keys for tenants whose JWTs should be trusted. Run these scripts in your Postgres database to register your tenant.

1 Create Table
Create the trusted_tenants table (if not exists)
SQL - Setup
-- Create auth schema if it doesn't exist CREATE SCHEMA IF NOT EXISTS auth; -- Create trusted_tenants table for storing public keys CREATE TABLE IF NOT EXISTS auth.trusted_tenants ( tenant_id TEXT PRIMARY KEY, public_key_pem TEXT NOT NULL, created_at TIMESTAMP DEFAULT now() );
2 Insert Your Key
Register your tenant's public key
SQL - Your Tenant
INSERT INTO auth.trusted_tenants (tenant_id, public_key_pem) VALUES ( 'your-tenant-id', '-----BEGIN PUBLIC KEY----- ... your public key here ... -----END PUBLIC KEY-----' ) ON CONFLICT (tenant_id) DO UPDATE SET public_key_pem = EXCLUDED.public_key_pem;
💡 Tip: The SQL above contains your actual tenant ID and public key. Just copy and run it directly in your Postgres client.
Step 4 of 4
Test SQL Queries
Example queries with RLS in action

At the start of each request, call auth.set_jwt() with the user's JWT. This makes the authenticated user's info available to RLS policies.

SQL - Customers
-- ═══════════════════════════════════════════════════════════════════ -- Set JWT and authenticate the session -- ═══════════════════════════════════════════════════════════════════ BEGIN; SELECT auth.clear_jwt(); SELECT auth.set_jwt('-- replace with your magic link JWT --'); -- Decoded JWT values: -- auth.email() → 'user@example.com' -- auth.role() → 'customer' -- ═══════════════════════════════════════════════════════════════════ -- CUSTOMERS: SELECT (Customer sees own record only) -- ═══════════════════════════════════════════════════════════════════ SELECT * FROM customers; -- ✓ Returns only row where email = 'user@example.com' -- ═══════════════════════════════════════════════════════════════════ -- CUSTOMERS: UPDATE name and phone -- ═══════════════════════════════════════════════════════════════════ UPDATE customers SET name = 'New Name', phone = '555-0100' WHERE email = 'user@example.com'; -- ✓ Succeeds: updating own record -- ═══════════════════════════════════════════════════════════════════ -- 🚨 HACKER ATTEMPT: UPDATE someone else's record -- ═══════════════════════════════════════════════════════════════════ UPDATE customers SET name = 'Hacked!' WHERE email = 'other@example.com'; -- ✗ FAILS RLS! Cannot update another user's record (email ≠ auth.email()) COMMIT;
Step 1 of 4
Create RLS Policies
Use auth functions in your row-level security policies

Create RLS policies that use the auth.* functions to filter data based on the authenticated user.

SQL - Customers Table
-- Enable RLS on customers table ALTER TABLE customers ENABLE ROW LEVEL SECURITY; -- Customers can SELECT their own record CREATE POLICY customers_select ON customers FOR SELECT USING (email = auth.email()); -- Customers can UPDATE their own name and phone CREATE POLICY customers_update ON customers FOR UPDATE USING (email = auth.email()); -- Admins can see all customers CREATE POLICY customers_admin ON customers FOR ALL USING (auth.role() = 'admin');
Step 2 of 4
API Endpoints & Configuration
POST
Request Verification Code - Send a 6-digit verification code to the user's email address. This initiates the magic link authentication flow.
Request Body:
{
  "email": "user@example.com"
}
Response:
{
  "success": true,
  "message": "Verification code sent"
}
POST
Validate Code & Get JWT - Exchange the verification code for a signed JWT containing user profile information including email, name, phone, and role.
Request Body:
{
  "email": "user@example.com",
  "code": "123456"
}
Response (Success):
{
  "success": true,
  "jwt": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "email": "user@example.com",
    "name": "John Doe",
    "phone": "+1234567890",
    "role": "customer"
  }
}
Public Key (for JWT verification)

                                
Private key was only shown at registration. If lost, you'll need to create a new tenant.
Danger Zone
Irreversible actions for this project
Delete this project
Once deleted, this project and all its data cannot be recovered.
Experience your authentication flow in a simulated e-commerce app
This email receives magic link verification codes via + addressing (e.g., you+customer@example.com and you+admin@example.com both deliver to you@example.com).

ℹ️ How It Works

1
Enter your email address (e.g., bob@gmail.com)
2
Click a user card to launch as Customer or Admin
3
Sign in - magic link sent to your inbox via + addressing
4
Experience the app with that user's role and permissions
Choose an Icon
JWT Token Details
Decoded Payload
{}
🔏
Raw JWT Token
Welcome back
Sign in with a magic link - no password needed
Check your email
We sent a 6-digit code to
user@example.com
Didn't receive it? Resend
Authenticated
⚠️ Demo mode - no email verification performed
This session proves:
Email ownership verified via 6-digit code
JWT signed with tenant's private key
Verifiable by your backend with public key
* Admin tab only visible because your role is "admin"
9:41
Back
Back
📬
Check your email
We sent a 6-digit code to:
you@example.com
🌟
Demo App
👤 Guest