@davidoost/eva-nextjs-sdk
v0.1.11
Published
Lightweight Next.js SDK for Eva OMS
Downloads
1,326
Readme
@davidoost/eva-nextjs-sdk
Lightweight Next.js SDK for the EVA OMS API.
Fully-typed HTTP client following the same SSR pattern as Supabase. Types are generated into your project against your specific EVA environment — so you always have accurate types regardless of which services your instance has enabled, and you're never blocked by a new SDK release when EVA ships weekly updates.
Installation
npm install @davidoost/eva-nextjs-sdkRequires next >= 15.0.0 as a peer dependency.
Setup
1. Environment variables
# .env.local
EVA_BASE_URL=https://api.euw.your-store.eva-online.cloud
EVA_API_KEY=your-api-key
NEXT_PUBLIC_EVA_BASE_URL=https://api.euw.your-store.eva-online.cloud
# Optional — sets EVA-Requested-OrganizationUnitID on every request
EVA_ORGANIZATION_UNIT_ID=1
NEXT_PUBLIC_EVA_ORGANIZATION_UNIT_ID=12. Generate types
Run the generate command once (and again whenever your EVA environment gets updated):
EVA_BASE_URL=https://api.euw.your-store.eva-online.cloud npx @davidoost/eva-nextjs-sdk generateThis downloads the eva-apispec generator binary, runs it against your environment, and writes the following into your project:
src/eva/
generated/ ← EVA service types, specific to your environment
eva-services-*/
service-map.ts
server.ts ← typed createClient() and createApiClient()
client.ts ← typed createClient() for Client ComponentsCommit the generated files. Re-run the command when your EVA environment updates.
3. Options
# Custom output directory (default: src/eva)
npx @davidoost/eva-nextjs-sdk generate --output src/lib/eva
# Use latest public spec instead of your environment
npx @davidoost/eva-nextjs-sdk generateUsage
Server Components
import { createClient } from '@/eva/server';
import { asProduct } from '@davidoost/eva-nextjs-sdk';
export default async function ProductPage({ params }) {
const client = await createClient();
const response = await client.GetProductDetail({ ProductID: params.id });
const product = asProduct(response.Result);
return <div>{product?.display_value}</div>;
}Server Actions
'use server';
import { createClient } from '@/eva/server';
export async function login(email: string, password: string) {
const client = await createClient();
await client.login({ EmailAddress: email, Password: password });
// session cookies are set automatically
}
export async function logout() {
const client = await createClient();
await client.logout();
}
export async function addToCart(productId: number, quantity: number) {
const client = await createClient();
await client.AddProductToOrder({ ProductID: productId, Quantity: quantity });
}Route Handlers
// app/api/products/route.ts
import { createClient } from '@/eva/server';
export async function GET() {
const client = await createClient();
const data = await client.SearchProducts({ Query: '' });
return Response.json(data);
}Client Components
Session cookies are httpOnly — the browser can't read them. For authenticated mutations prefer Server Actions. For public calls use the browser client directly:
'use client';
import { createClient } from '@/eva/client';
export function SearchBar() {
const handleSearch = async (query: string) => {
const client = createClient();
const results = await client.SearchProducts({ Query: query });
};
}API key client
createApiClient() uses your EVA_API_KEY and has access to all EVA services — including the ~30 that are API-key-only and unavailable via session token:
import { createApiClient } from '@/eva/server';
export async function GET() {
const client = createApiClient();
const data = await client.SomeService({});
return Response.json(data);
}Where things can run
| | Server Component | Server Action | Route Handler | Client Component |
|---|:---:|:---:|:---:|:---:|
| createClient() from eva/server | ✅ | ✅ | ✅ | ❌ |
| createApiClient() from eva/server | ✅ | ✅ | ✅ | ❌ |
| createClient() from eva/client | ❌ | ❌ | ❌ | ✅ |
| client.login() | ❌ | ✅ | ✅ | ❌ |
| client.logout() | ❌ | ✅ | ✅ | ❌ |
login() and logout() write cookies — only allowed in Server Actions and Route Handlers.
Silent token refresh
The server client automatically refreshes the access token when it's missing but a refresh token is present. In a Server Action or Route Handler the new token is written to cookies. In a Server Component the write fails silently — middleware handles persistence on the next navigation.
Middleware
Use checkAuth in middleware.ts to refresh tokens and guard routes. Always return result.response — it carries refreshed cookies to both the browser and Server Components in the same request.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { checkAuth } from '@davidoost/eva-nextjs-sdk';
export async function middleware(request: NextRequest) {
const { valid, refreshed, response } = await checkAuth(request);
if (!valid && !refreshed) {
return NextResponse.redirect(new URL('/login', request.url));
}
return response;
}
export const config = {
matcher: ['/account/:path*', '/checkout/:path*'],
};| Result | Meaning |
|---|---|
| valid: true | Access token present |
| refreshed: true | Token refreshed — new cookies on response |
| valid: false, refreshed: false | No session — redirect to login |
Gating by role
const { valid, refreshed, role, response } = await checkAuth(request, {
requiredRole: 'employee', // 'customer' | 'employee'
});
if (!valid && !refreshed) return NextResponse.redirect(new URL('/login', request.url));
if (role !== 'employee') return NextResponse.redirect(new URL('/403', request.url));
return response;The role is derived from data.User.Type at login and stored in eva_user_role cookie automatically.
Debug mode
Set debug: true on any client to log all requests and responses to the console. Auth tokens are redacted.
const client = await createServerClient({ ..., debug: true });
// or
const client = createApiClient({ debug: true });
// or
const client = createClient({ debug: true });Server-side output appears in your terminal. Browser client output appears in the browser console (requests are also visible in the Network tab since they originate from the browser).
[eva-sdk] POST https://api.euw.store.eva-online.cloud/message/SearchProducts
[eva-sdk] Headers: { EVA-User-Agent: 'eva-nextjs-sdk/0.1.8', EVA-App-Token: '...' }
[eva-sdk] Body: {"Query":"roses"}
[eva-sdk] 200 OK — https://api.euw.store.eva-online.cloud/message/SearchProductsApp token (anonymous sessions)
EVA returns an EVA-App-Token header on responses. The SDK stores it automatically in an eva_app_token cookie and sends it back as an EVA-App-Token request header on every subsequent request — enabling anonymous/guest sessions without any extra code. It is sent alongside the Authorization header when a user is logged in.
| Client | Stores app token | Uses app token as fallback |
|---|---|---|
| createClient() (server) | ✅ via cookieMethods.setAll | ✅ |
| createClient() (browser) | ✅ via document.cookie | ✅ |
| createApiClient() | — | — |
The eva_app_token cookie is non-httpOnly on the browser client so JavaScript can read and write it.
Organization unit
The EVA-Requested-OrganizationUnitID header is resolved with this precedence for each client:
| Client | Precedence |
|---|---|
| createClient() (server) | organizationUnitId option → eva_ou cookie → EVA_ORGANIZATION_UNIT_ID env var |
| createApiClient() | organizationUnitId option → EVA_ORGANIZATION_UNIT_ID env var |
| createClient() (browser) | organizationUnitId option → eva_ou cookie → NEXT_PUBLIC_EVA_ORGANIZATION_UNIT_ID env var |
The eva_ou cookie must be non-httpOnly so the browser client can read it.
Via env var (global default, set once):
EVA_ORGANIZATION_UNIT_ID=1
NEXT_PUBLIC_EVA_ORGANIZATION_UNIT_ID=1Per client instance (overrides env var):
const client = await createServerClient({ ..., organizationUnitId: 456 });Via cookie (useful for letting users switch between org units dynamically):
// Set a non-httpOnly cookie named 'eva_ou'
cookieStore.set('eva_ou', '456', { path: '/' }); // no httpOnlyProduct types
EVA product data is schema-less at the API level. Use asProduct to cast to a typed interface:
import { asProduct } from '@davidoost/eva-nextjs-sdk';
const response = await client.GetProductDetail({ ProductID: 123 });
const product = asProduct(response.Result);
product?.display_value; // string
product?.display_price; // number
product?.primary_image; // { Url?, Blob?, Name?, Type? }Standard fields on EvaProduct:
| Field | Type | Description |
|---|---|---|
| product_id | number | EVA internal product ID |
| backend_id | string? | External / backend identifier |
| display_value | string | Product name |
| slug | string? | URL-friendly slug |
| barcode | string? | Barcode |
| custom_id | string? | Custom identifier |
| product_type | number? | 1 Simple · 2 Bundle · 4 Configurable · 8 Variant |
| logical_level | number? | 0 root configurable · 1 variant |
| primary_image | object? | { Url, Blob, Name, Type } |
| media | object? | Gallery, swatches, image count |
| category_names | string[]? | Category names |
| category_paths | string[][]? | Full category paths |
| brand_name | string? | Brand display name |
| brand_id | number? | EVA brand ID |
| display_price | number? | Current selling price |
| original_price | number? | List price before discounts |
| discount_percentage | number? | e.g. 25 for 25% off |
Custom product properties
Declare your EVA-configured properties once — all asProduct() calls pick them up automatically:
// eva.d.ts (anywhere in your project)
import '@davidoost/eva-nextjs-sdk';
declare module '@davidoost/eva-nextjs-sdk' {
interface EvaProductExtension {
colour?: string;
size_label?: string;
}
}Importing request/response types
The generated files in src/eva/generated/ export all request and response types for your environment. Import from the relevant assembly:
import type { SearchProducts, SearchProductsResponse } from '@/eva/generated/eva-services-core';
async function search(req: SearchProducts): Promise<SearchProductsResponse> {
const client = await createClient();
return client.SearchProducts(req);
}Explicit call() API
import { SvcGetOrder } from '@/eva/generated/eva-services-core';
const order = await client.call(SvcGetOrder, { ID: 123 });Keeping types up to date
Re-run the generate command whenever your EVA environment gets updated:
EVA_BASE_URL=https://api.euw.your-store.eva-online.cloud npx @davidoost/eva-nextjs-sdk generateThe generator binary is cached in node_modules/.cache/eva-sdk-generator/ and reused on subsequent runs. It auto-updates to the latest eva-apispec release.
Types are environment-specific — each customer's src/eva/generated/ will only contain services enabled for their EVA instance. This means you won't see services in autocomplete that aren't available to you.
