OTP login in 5 minutes
Scaffold a Next.js app with email and WhatsApp OTP, optional password auth, and optional Neon + Drizzle. @zindua/sdk runs on the server. Your API key never reaches the browser.
npx @zindua/create-app@latest my-appOne command
Scaffold with npx @zindua/create-app@latest my-app. Setup wizard runs in the browser after npm run dev.
Keys on server
ZINDUA_API_KEY lives in .env.local only. Route Handlers call the API, never the browser.
Email + WhatsApp
OTP channel (email or whatsapp) and template language are set in .env.local, not in the UI.
i18n routes
Localized pages at /en, /fr, /es with a locale switcher. UI language is separate from OTP template lang.
Zustand flow
Client auth state machine: credentials → otp → success. Server sessions via httpOnly cookie.
Neon + Drizzle
Optional Postgres for users, sessions, OTP codes, audit logs, and devices. Required for password auth.
Password auth
AUTH_MODE=otp | password | both. Register at /[locale]/register with scrypt-hashed passwords.
CLI maintenance
Switch UI with template, sync the auth engine with update, diagnose with doctor.
From zero to localhost
Requires Node 18+ and a Zindua project API key.
npx @zindua/create-app@latest my-app
cd my-app
npm install
npm run dev
# Browser wizard at /setup — API key, channels, template, auth mode
# Restart dev server when setup finishes (Ctrl+C, npm run dev)
# Optional Neon after password auth in wizard:
# npm run db:pushnpm i @zindua/create-app for every package. That only installs the CLI into node_modules. Run npx @zindua/create-app@latest my-app to create a project folder with code.SDK dependency: npm install @zindua/sdk (v1.2.7 included in the template).
Step-by-step
Configure Zindua first, then scaffold your app. Same template slug works for email and WhatsApp.
Create a Zindua project
Sign up at zindua.run, create a project, and copy the API key from Dashboard → Projects. Use a test key locally, live key in production.
Connect delivery channels
In your project: connect an email provider (Gmail, Outlook, SMTP…) and/or link WhatsApp via QR for OTP on both channels.
Create templates
Slug otp-verification with {{code}} required. Optional {{appName}}. For login alerts, add new-login-alert with {{device}}, {{ip}}, {{time}}.
Scaffold your app
Run npx @zindua/create-app@latest my-app. The CLI scaffolds the project only — no API key prompts in the terminal.
Configure in the browser
cd my-app, npm install, npm run dev. Open localhost:3000 — the /setup wizard verifies your API key, picks OTP channel, UI template, auth mode, and optional Neon URL. Restart npm run dev when done.
Optional: Neon database
If you chose password auth in the wizard, DATABASE_URL is already in .env.local. Run npm run db:push. Password mode requires DATABASE_URL.
Test and deploy
Send OTP by email or WhatsApp, verify on /[locale]/verify, land on /[locale]/success. Deploy to Vercel or Railway with server-side env vars and a live API key.
Neon, Drizzle, Zustand
Optional Postgres persistence and a client-side auth state machine. Falls back to in-memory OTP when DATABASE_URL is unset.
Neon Postgres
Optional DATABASE_URL. Persists users, sessions, OTP codes, audit logs, and devices. Required for password register/login.
Drizzle schemas
lib/db/schema.ts defines app_users, app_sessions, app_otp_codes, app_audit_logs, app_devices. Push with npm run db:push.
Zustand state machine
store/auth-store.ts tracks credentials → otp → success on the client while the server owns sessions.
proxy.ts guard
Next.js 16 route guard protects /[locale]/success. httpOnly session cookie, rate limiting on send-otp.
Neon setup
# .env.local — optional unless AUTH_MODE=password|both
DATABASE_URL=postgresql://user:pass@host/db?sslmode=require
# Push Drizzle schema to Neon
npm run db:pushDrizzle tables
// lib/db/schema.ts (Drizzle + Neon)
app_users — identifier, channel, password_hash
app_sessions — token_hash, expires_at, ip, user_agent
app_otp_codes — code_hash, attempts, expires_at
app_audit_logs — action, ip, user_agent, metadata
app_devices — fingerprint, label, last_seen_atZustand state machine
// store/auth-store.ts
type AuthStep = "credentials" | "otp" | "success";
// Client flow:
// 1. credentials — enter email/phone, call /api/auth/send-otp
// 2. otp — enter 6-digit code, call /api/auth/verify-otp
// 3. success — redirect to /[locale]/successAuth modes
Set AUTH_MODE in .env.local. Password register/login requires DATABASE_URL (Neon).
| AUTH_MODE | Behavior |
|---|---|
| otp | OTP only (default) |
| password | Email + password register and login |
| both | Tabs on login: OTP and password |
Password routes: Register UI at /[locale]/register. API: POST /api/auth/register, POST /api/auth/login-password.
Login alert email
After OTP verify, register, or password login, optionally notify the user via a Zindua template.
# Post-login security alert (optional)
ZINDUA_LOGIN_ALERT_ENABLED=true
ZINDUA_LOGIN_ALERT_TEMPLATE=new-login-alert
# Create matching Zindua email template with:
# {{appName}}, {{device}}, {{ip}}, {{time}}Commands for an existing project
Run inside your app folder after scaffolding.
Scaffold project
npx @zindua/create-app@latest my-appCreates the app with placeholder .env.local. Run npm run dev — the setup wizard opens in your browser at /setup.
Switch UI
npx @zindua/create-app@latest templateSwap corporate or minimal layout. Does not overwrite .env.local.
Sync auth engine
npx @zindua/create-app@latest updatePull latest routes, lib/, store/ from the package. Keeps .env.local untouched.
Diagnose setup
npx @zindua/create-app@latest doctorCheck .env.local, API key reachability, and common misconfigurations.
# Create project (recommended)
npx @zindua/create-app@latest my-app
# Same scaffold, optional folder name
npx @zindua/create-app@latest init my-app
# Inside an existing project
npx @zindua/create-app@latest template # switch corporate | minimal UI
npx @zindua/create-app@latest update # sync auth engine, keep .env.local
npx @zindua/create-app@latest doctor # diagnose setupSecurity by default
End users only see your app. API keys and provider credentials stay in your server environment and Zindua dashboard.
Server-only API key
ZINDUA_API_KEY belongs in .env.local or your host's secret env vars. Never prefix it with NEXT_PUBLIC_.
Route Handlers only
@zindua/sdk runs in app/api/**/route.ts. Client components call your own /api/auth/* endpoints.
No secrets in git
.env.local is gitignored. Commit only .env.example with placeholder values.
Rotate if leaked
Regenerate the project API key in the dashboard immediately if a real key was exposed.
Environment variables
| Variable | Notes |
|---|---|
| ZINDUA_SETUP_COMPLETE | Set true by browser wizard when done (dev only) |
| ZINDUA_API_KEY | requiredFrom Dashboard → Projects |
| ZINDUA_APP_NAME | Shown in templates as {{appName}} |
| ZINDUA_TEMPLATE_SLUG | OTP template slug |
| ZINDUA_OTP_CHANNEL | email or whatsapp (code config, not UI) |
| ZINDUA_OTP_LANG | Template language for Zindua API |
| ZINDUA_DEFAULT_LOCALE | UI locale: en, fr, es |
| AUTH_MODE | otp | password | both (password needs DATABASE_URL) |
| ZINDUA_LOGIN_ALERT_ENABLED | Send new-login-alert email after sign-in |
| ZINDUA_LOGIN_ALERT_TEMPLATE | Login alert template slug |
| APP_URL | requiredPublic app URL |
| AUTH_SESSION_SECRET | requiredLong random string for session signing |
| DATABASE_URL | Neon Postgres; required for password auth; optional for persisted OTP/sessions |
| ZINDUA_API_BASE_URL | Optional API override |
// app/api/auth/send-otp/route.ts (server only)
import { getZindua } from "@/lib/zindua";
await getZindua().send({
to: recipient,
channel: "email", // or "whatsapp"
template: "otp-verification",
lang: "en",
variables: { code: "482910", appName: "MyApp" },
});What you get
Next.js 16, Tailwind v4, corporate or minimal UI, i18n, Zustand, optional Drizzle + Neon. No SMTP.
my-app/
├── .env.example # Placeholders only, safe to commit
├── .env.local # Your real key, never commit
├── app/
│ ├── [locale]/
│ │ ├── page.tsx # Login (OTP / password tabs)
│ │ ├── register/ # Password register (if AUTH_MODE allows)
│ │ ├── verify/ # Enter 6-digit OTP
│ │ └── success/ # Protected after login
│ ├── setup/ # Browser setup wizard (dev only)
│ └── api/
│ ├── setup/ # Verify API key, save .env.local
│ └── auth/
│ ├── send-otp/route.ts
│ ├── verify-otp/route.ts
│ ├── register/route.ts
│ ├── login-password/route.ts
│ └── logout/route.ts
├── lib/
│ ├── zindua.ts # Server-only SDK client
│ ├── otp/ # OTP store (memory or DB)
│ ├── db/schema.ts # Drizzle tables for Neon
│ ├── auth/ # sessions, password, login-alert
│ ├── i18n/ # en, fr, es dictionaries
│ └── security.ts
├── store/auth-store.ts # Zustand: credentials → otp → success
├── components/auth/ # corporate | minimal UI
├── proxy.ts # Guards /[locale]/success
└── drizzle.config.tsWordPress for sites. Next.js for apps.
Same Zindua project, same templates. Pick the integration that fits your stack.