Zindua

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-app

One 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.

Quick start

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.

terminal
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:push

Use 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).

Setup

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.

Stack

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.

Reference

Auth, SDK, and errors

Pick a row. Details open on the right — no endless tables.

Auth modes

AUTH_MODE

AUTH_MODE=otp

OTP only (default)

Password routes need Neon: /[locale]/register, POST /api/auth/register.

send() options

Field

toRequired

Email 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_EMAIL

HTTP 400 / 0

Bad email or phone used with channel email.

Match channel and to format.

Environment variables

VariableNotes
ZINDUA_SETUP_COMPLETESet true by browser wizard when done (dev only)
ZINDUA_API_KEYrequiredFrom Dashboard → Projects
ZINDUA_APP_NAMEShown in templates as {{appName}}
ZINDUA_TEMPLATE_SLUGOTP template slug
ZINDUA_OTP_CHANNELemail or whatsapp (code config, not UI)
ZINDUA_OTP_LANGTemplate language for Zindua API
ZINDUA_DEFAULT_LOCALEUI locale: en, fr, es
AUTH_MODEotp | password | both (password needs DATABASE_URL)
ZINDUA_LOGIN_ALERT_ENABLEDSend new-login-alert email after sign-in
ZINDUA_LOGIN_ALERT_TEMPLATELogin alert template slug
APP_URLrequiredPublic app URL
AUTH_SESSION_SECRETrequiredLong random string for session signing
DATABASE_URLNeon Postgres; required for password auth; optional for persisted OTP/sessions
ZINDUA_API_BASE_URLOptional API override

CLI in an existing project

Scaffold project

npx @zindua/create-app@latest my-app

Creates 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 template

Swap corporate or minimal layout. Does not overwrite .env.local.

Sync auth engine

npx @zindua/create-app@latest update

Pull latest routes, lib/, store/ from the package. Keeps .env.local untouched.

Diagnose setup

npx @zindua/create-app@latest doctor

Check .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

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

project tree
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.ts

WordPress for sites. Next.js for apps.

Same Zindua project, same templates. Pick the integration that fits your stack.

API reference → · Node SDK