Complete technical guide for building on Nino360
Nino360 is a multi-tenant enterprise SaaS platform built on modern web technologies with AI-powered features across 18 business modules.
Complete tenant isolation at database level with URL-based resolution.
RESTful API design with structured responses and tenant isolation.
// Standard API Response Format
interface ApiResponse<T> {
data?: T
error?: string
}
// Example Usage
export async function GET() {
const data = await fetchData()
return Response.json({ data })
}
// Error Handling
export async function POST() {
try {
const result = await createRecord()
return Response.json({ data: result })
} catch (error) {
return Response.json(
{ error: error.message },
{ status: 400 }
)
}
}PostgreSQL via Supabase with Row Level Security (RLS) policies.
import { withAuth } from "@/lib/api/handlers"
export const GET = withAuth(
async (req, { session, supabase }) => {
// Access authenticated session
const userId = session.user.id
// Use authenticated supabase client
const { data } = await supabase
.from('users')
.select('*')
.eq('id', userId)
return Response.json({ data })
}
)import { getTenantContext } from "@/lib/auth/tenant-context"
export async function GET() {
const context = await getTenantContext()
if (!context) {
return Response.json(
{ error: "Unauthorized" },
{ status: 401 }
)
}
const { tenantId, userId, roles } = context
// Query with tenant isolation
const { data } = await supabase
.from('crm_leads')
.select('*')
.eq('tenant_id', tenantId)
return Response.json({ data })
}import { hasPermission } from "@/lib/auth/tenant-context"
export async function POST(req: Request) {
// Check permission
if (!await hasPermission('crm:write')) {
return Response.json(
{ error: 'Forbidden' },
{ status: 403 }
)
}
// Continue with protected operation
const body = await req.json()
// ... create lead
return Response.json({ data: result })
}import { generateText } from 'ai'
export async function POST(req: Request) {
const { prompt } = await req.json()
// Use Vercel AI SDK with AI Gateway
const { text } = await generateText({
model: 'openai/gpt-4.1',
prompt: prompt,
temperature: 0.7
})
return Response.json({ data: { text } })
}
// Supported models via AI Gateway:
// - openai/gpt-4
// - anthropic/claude-sonnet-4.5
// - xai/grok-4-fast
// - And more...id: uuid (PK) name: text slug: text (unique) status: text created_at: timestamp
user_id: uuid (FK → auth.users) tenant_id: uuid (FK → app.tenants) role: text role_id: uuid (FK → app.roles) status: text joined_at: timestamp
id: uuid (PK) tenant_id: uuid (FK → app.tenants) key: text name: text description: text
id: uuid (PK) key: text (unique) name: text description: text category: text
All tables have RLS policies enforcing tenant isolation:
-- Example RLS Policy CREATE POLICY tenant_isolation_policy ON crm_leads USING (tenant_id = current_tenant_id()); -- Policy ensures users can only access -- data belonging to their tenant
import { createClient } from '@/lib/supabase/client'
const supabase = createClient()
// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
// Sign out
await supabase.auth.signOut()import { createServerClient } from '@/lib/supabase/server'
const supabase = await createServerClient()
// Get current user
const { data: { user } } = await supabase.auth.getUser()
// Check session
const { data: { session } } = await supabase.auth.getSession()Role-based access control with fine-grained permissions.
# Supabase Configuration NEXT_PUBLIC_SUPABASE_URL=your_supabase_url NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key SUPABASE_SERVICE_ROLE_KEY=your_service_role_key # Development NEXT_PUBLIC_DEV_SUPABASE_REDIRECT_URL=http://localhost:3000 # AI (Optional - AI Gateway handles by default) OPENAI_API_KEY=sk-... XAI_API_KEY=xai-... # Nino360 API NINO360_API_KEY=your_api_key