A complete, modern storefront application built with Next.js that demonstrates full integration with the Tip4Serv API. This is a reference implementation for developers building checkout-enabled marketplaces.
🔗 Live Demo → duster-theme.vercel.app
Duster Theme is a production-ready example of:
- Dark-themed gaming marketplace UI with neon accents
- Server-side API proxying for secure key handling
- Dynamic checkout flow with form-based identifier collection
- Client-side cart management with persistent storage
- TanStack Query for efficient API data fetching and caching
- Full TypeScript type safety with Zod validation
- Framework: Next.js 15+ with App Router
- Language: TypeScript
- Styling: Tailwind CSS v4
- State Management: Zustand (cart) + TanStack Query (server data)
- Animations: Framer Motion
- Validation: Zod
- Icons: Lucide React
-
Server-Only API Keys: Tip4Serv API key is only accessible in Node.js environment (Route Handlers, server components). Never exposed to browser.
-
Lightweight Caching: In-memory cache (TTL: 5 minutes) for store info, theme, categories, and products. No external cache required.
-
Dynamic Checkout Form: Identifiers are fetched dynamically from the checkout endpoint, allowing the form to adapt to product requirements without hardcoding fields.
-
Client-Side Cart: Uses Zustand with localStorage persistence. Cart operations are optimistic and do not require server calls until checkout.
-
Minimal Backend Logic: Route handlers only proxy requests, validate responses with Zod, and apply caching. All complex state lives client-side.
.
├── app/
│ ├── api/tip4serv/ # API proxy route handlers
│ │ ├── store/
│ │ │ ├── whoami/
│ │ │ ├── theme/
│ │ │ ├── categories/
│ │ │ └── product/[id]/
│ │ └── checkout/
│ │ ├── identifiers/
│ │ └── route.ts
│ ├── checkout/ # Checkout flow pages
│ │ ├── page.tsx
│ │ ├── success/
│ │ ├── canceled/
│ │ └── pending/
│ ├── product/[id]/ # Product detail page
│ ├── shop/ # Shop listing page
│ ├── cart/ # Cart page
│ ├── layout.tsx # Root layout with providers
│ ├── page.tsx # Home page
│ └── globals.css # Dark theme + utility styles
├── components/
│ ├── layout/
│ │ ├── header.tsx
│ │ └── footer.tsx
│ ├── product/
│ │ └── product-card.tsx
│ └── providers/
│ └── query-provider.tsx
├── hooks/
│ ├── use-api.ts # TanStack Query hooks
│ └── use-cart.ts # Zustand cart store
├── lib/
│ ├── config.ts # Environment configuration
│ ├── schemas.ts # Zod schemas for validation
│ └── api-client.ts # Fetch + cache utilities
├── public/ # Static assets
├── .env.example # Environment template
├── .env.local # Local env (git ignored)
└── package.json
- Node.js 18+
- npm or yarn
- Clone the repository:
git clone https://github.com/Tip4Serv/duster-theme.git
cd duster-theme
2. Install dependencies:
```bash
npm install
-
Configure environment variables:
cp .env.example .env.local
-
Edit
.env.localand add your credentials:TIP4SERV_API_KEY=your_jwt_token_here TIP4SERV_API_BASE=https://api.tip4serv.com/v1 NEXT_PUBLIC_SITE_URL=http://localhost:3000
(You can find your API key here : https://tip4serv.com/dashboard/api-keys)
| Variable | Purpose | Required | Example |
|---|---|---|---|
TIP4SERV_API_KEY |
JWT token for API authentication | Yes | eyJ0eXAi... |
TIP4SERV_API_BASE |
API base URL | Yes | https://api.tip4serv.com/v1 |
NEXT_PUBLIC_SITE_URL |
Application URL for redirects | Yes | http://localhost:3000 |
Security Note: The API key is only accessible in Node.js (Route Handlers). It is never exposed to the browser, even though the name NEXT_PUBLIC doesn't apply here due to the next/server import guard.
npm run devThe app will be available at http://localhost:3000.
npm run build
npm startThese endpoints require the TIP4SERV_API_KEY and are called via /api/tip4serv/*:
GET /api/tip4serv/store/whoami- Fetch store informationGET /api/tip4serv/store/theme- Fetch theme configurationGET /api/tip4serv/store/categories- Fetch product categoriesGET /api/tip4serv/store/products- Fetch product listGET /api/tip4serv/store/product/[id]- Fetch single product details
These are proxied for consistency but don't require auth:
GET /api/tip4serv/checkout/identifiers?store={store_id}&products=[{product_id1},{product_id2}]- Fetch required checkout fieldsPOST /api/tip4serv/checkout?store={store_id}- Create checkout session
The checkout flow follows this mandatory sequence:
const cart = useCart();
cart.addItem(product, quantity);Cart state is stored in localStorage and persists across sessions.
When the user reaches checkout, the app queries which user fields are required:
const { data: identifiers } = useCheckoutIdentifiers(storeId, productIds);
// Example response: { identifiers: ['email', 'minecraft_username', 'discord_id'] }The checkout page renders form fields based on the required identifiers:
const requiredIdentifiers = identifiersData?.identifiers || [];
{requiredIdentifiers.map(identifier => (
<input
key={identifier}
name={identifier}
placeholder={IDENTIFIER_LABELS[identifier]}
required
/>
))}On form submission, all required fields are validated. The cart contents are transformed into the Tip4Serv checkout format:
const checkoutData = {
products: [
{
product_id: 87,
type: 'addtocart',
quantity: 1,
custom_fields: { /* if applicable */ },
server_selection: 1, /* if applicable */
donation_amount: 0.00, /* if applicable */
}
],
user: {
email: 'user@example.com',
minecraft_username: 'PlayerName',
discord_id: '123456789',
// ... other required identifiers
},
redirect_success_checkout: 'https://yourdomain.com/checkout/success',
redirect_canceled_checkout: 'https://yourdomain.com/checkout/canceled',
redirect_pending_checkout: 'https://yourdomain.com/checkout/pending',
};The checkout endpoint returns a URL:
POST /api/tip4serv/checkout?store=762&redirect=true
// Response: { url: 'https://checkout.tip4serv.com/...' }The browser is redirected to this URL. After payment, users are directed back to success/canceled/pending pages.
Before integrating endpoints, validate them with curl:
# Test store info
$headers = @{ "Authorization" = "Bearer YOUR_API_KEY" }
$response = Invoke-RestMethod -Uri "https://api.tip4serv.com/v1/store/whoami" `
-Headers $headers -Method Get
$response | ConvertTo-JsonAll responses are validated against Zod schemas. See lib/schemas.ts for full type definitions.
Example /store/whoami response:
{
"id": 762,
"title": "Duster Theme",
"description": "Premium gaming products",
"domain": "teststore",
"logo": "https://cdn.example.com/logo.png",
"currency": "USD",
"timezone": "Europe/London",
"color": "#d4ff00"
}// In any client component
import { useProducts, useProduct } from '@/hooks/use-api';
export function MyComponent() {
const { data: products, isLoading } = useProducts({ maxPage: 20 });
const { data: product } = useProduct(87);
if (isLoading) return <div>Loading...</div>;
return <div>{products?.products.length} products</div>;
}- Server-side cache (TTL: 5 min) reduces redundant API calls
- TanStack Query caches (TTL: 1 min) for client-side data
- Stale-while-revalidate: queries marked stale after TTL but returned immediately if cached
To refresh data in development:
queryClient.invalidateQueries({ queryKey: ['store', 'products'] });const cart = useCart();
// Add product
cart.addItem(product, quantity);
// Update quantity
cart.updateQuantity(productId, newQuantity);
// Remove product
cart.removeItem(productId);
// Clear entire cart
cart.clearCart();
// Get totals
const total = cart.getTotal();
const count = cart.getItemCount();All changes are persisted to localStorage automatically.
The design uses:
- Primary: Lime green (#d4ff00)
- Secondary: Cyan (#00e5ff)
- Accent: Purple (#9945ff)
- Background: Charcoal (#0a0a0f)
CSS utilities available:
.glow-primary /* Box shadow glow effect */
.gradient-primary /* Lime to cyan gradient */
.grid-pattern /* Subtle grid background */Framer Motion is used for entrance animations on product cards and checkouts. Components are marked with 'use client' to support animations.
Mobile-first approach using Tailwind breakpoints:
- Mobile: Single column
- Tablet (md): 2 columns
- Desktop (lg/xl): 3-4 columns
All Route Handlers:
- Catch errors and log them
- Return appropriate HTTP status codes
- Provide user-friendly error messages in the response
Zod schemas validate all responses. If the API returns malformed data:
- Error is caught and logged
- User sees a generic error message
- Check browser console for details
TanStack Query retries failed requests automatically (configurable in query-provider.tsx).
- Code Splitting: Next.js App Router automatically splits code per route
- Image Optimization: Next.js Image component optimizes product images
- Caching: In-memory server cache reduces API calls
- Lazy Loading: Product cards use Framer Motion for performance
- Stale-While-Revalidate: Cache serves immediately, background refetch
- API Key Protection: Never imported in client code. Stored only in
.env.local(git ignored). - CORS: Requests proxied through Route Handlers to avoid CORS issues.
- Input Validation: All form inputs validated with Zod before submission.
- Redirect URLs: Hardcoded in environment, not user-supplied.
The API key is not set in .env.local. Ensure:
TIP4SERV_API_KEY=eyJ0eXAi...Verify your API key is valid (you can generate a new one at https://tip4serv.com/dashboard/api-keys)
The required identifiers are fetched from the API based on product requirements. If fields are missing:
- Check the checkout/identifiers endpoint in DevTools
- Verify product has server choice or custom fields enabled
localStorage may be disabled or storage quota exceeded. Check browser console:
console.log(localStorage.getItem('duster-theme-cart'));-
Run type checking:
npm run type-check
-
Build the project:
npm run build
-
Deploy the
.nextfolder to your hosting (Vercel, AWS, etc.)
This is a reference implementation. Fork and customize as needed!
This project is provided as-is for integration with Tip4Serv.
Your store ID is automatically fetched from GET /store/whoami. To change the store, update the API key in .env.local.
Current store details are displayed in the header and footer, dynamically pulled from the API.
