OTP login in 5 minutes
Scaffold a Next.js app with email and WhatsApp OTP, optional password auth, and optional Neon + Drizzle. @zindua/sdk stays on the server.
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.
One panel. Every snippet.
Scaffold, OTP, Neon, Zustand, CLI — click to switch.
Playground
Scaffold to send
Select a topic — code updates in place. One glowing panel, not a wall of snippets.
npx @zindua/create-app@latest my-appcd my-appnpm installnpm 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:pushUse npx, not npm i. npm i @zindua/create-app only installs the CLI. Run npx @zindua/create-app@latest my-app to scaffold a project. SDK: npm install @zindua/sdk (v1.4.0).
Step-by-step
Configure Zindua first, then scaffold. Same template slug for email and WhatsApp.
Step 01
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.
Neon, Drizzle, Zustand
Optional Postgres. 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.
Auth, SDK, and errors
Pick a row. Details open on the right — no endless tables.
Auth modes
AUTH_MODE
AUTH_MODE=otpOTP only (default)
Password routes need Neon: /[locale]/register, POST /api/auth/register.
send() options
Field
toRequiredEmail if channel is email (default). E.164 phone with + for WhatsApp (e.g. +243812345678).
SDK methods
Method
send()POST /send
Queue email or WhatsApp. Returns logId and context.
Starter owns auth state
OTP codes, sessions, and users live in Neon (Drizzle) or in-memory. Zindua never stores your users.
SDK owns delivery
getZindua().send() queues the message. Template rendering, provider routing, and delivery logs are on Zindua.
UI lang ≠ template lang
ZINDUA_DEFAULT_LOCALE drives /en, /fr, /es pages. ZINDUA_OTP_LANG drives which template version send() uses.
Extend with Route Handlers
Add app/api/notify/route.ts for invoices, alerts, or cc/bcc sends. Same lib/zindua.ts client as OTP routes.
Common error codes
Error
INVALID_EMAILHTTP 400 / 0
Bad email or phone used with channel email.
Match channel and to format.
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 |
CLI in an existing project
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.
Security by default
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.
After send() — logs
Logs
Filter by channel and status. Search by logId from send().
Analytics
Send volume, success rate, top templates.
Webhooks
email.delivered and email.failed callbacks to your API routes.
No public SDK method for logs or analytics. The starter persists OTP in Neon or memory; Zindua stores delivery logs in the dashboard. Save result.logId from send() and open Dashboard → Logs, or configure webhooks.
What you get
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.