ignitionstack.pro v1.0 is out! Read the announcement →
Skip to Content

Products Admin Guide

Complete guide to the products admin panel, integrated with Supabase (Postgres + Storage + Auth).

The product system follows the Next.js 15 Server Components + Server Actions pattern.

Architecture

src/app/ ├── types/ │ └── product.ts # TypeScript types ├── server/products/ │ ├── get-all-products.ts # Active products (public) │ ├── get-all-products-admin.ts # All products (admin) │ ├── get-product-by-id.ts # Fetch by ID │ └── get-product-by-slug.ts # Fetch by slug ├── actions/products/ │ ├── create-product.ts # Create product │ ├── update-product.ts # Update product │ ├── delete-product.ts # Hard delete (permanent) │ └── toggle-product-active.ts # Soft delete (isActive) ├── [locale]/admin/products/ │ ├── page.tsx # Product list │ ├── new/page.tsx # Create new │ └── [id]/page.tsx # Edit existing └── components/admin/ ├── product-form.tsx # Form └── product-actions.tsx # Action buttons

Data Structure

Product Interface

interface Product { id: string // Slug (compatible with TemplateCard) slug: string // URL-friendly name: string description: string author: string image: string demoUrl: string downloadUrl?: string price: number priceType: 'free' | 'one-time' | 'subscription' stripePriceId?: string category: string stack: string level: string features: string[] keywords: string[] metaDescription?: string isPopular: boolean isActive: boolean // Soft delete flag createdAt: string updatedAt: string _supabaseId?: string // Internal ID (admin only) }

Acessando o Admin

http://localhost:3000/en/admin/products

Access requires authentication. Configure authorized emails in .env:

ADMIN_EMAILS=admin@example.com,outro@example.com

CRUD Operations

List products

import { getAllProductsAdmin } from '@/app/server/products/get-all-products-admin' // Returns every product, including inactive ones const products = await getAllProductsAdmin()

Create product

import { createProduct } from '@/app/actions/products/create-product' const result = await createProduct({ name: 'SaaS Landing Page', slug: 'saas-landing-page', description: 'Modern landing page template', author: 'ignitionstack.pro', image: '/assets/images/products/saas.png', demoUrl: 'https://demo.example.com', price: 49.99, priceType: 'one-time', stripePriceId: 'price_xxxxx', category: 'landing-page', stack: 'nextjs', level: 'intermediate', features: ['Responsive', 'Dark Mode', 'SEO'], isActive: true, isPopular: false, }) if (result.success) { console.log('Product created:', result.data) } else { console.error('Error:', result.error) }

Update product

import { updateProduct } from '@/app/actions/products/update-product' const result = await updateProduct(supabaseId, { price: 59.99, isPopular: true, })

Soft delete vs. hard delete

TypeActionReversibleWhen to use
Soft DeletetoggleProductActiveYesTemporarily disable
Hard DeletedeleteProductNoRemove permanently
// Soft delete (recommended) import { toggleProductActive } from '@/app/actions/products/toggle-product-active' await toggleProductActive(productId, false) // Deactivate await toggleProductActive(productId, true) // Reactivate // Hard delete (careful!) import { deleteProduct } from '@/app/actions/products/delete-product' await deleteProduct(productId) // REMOVE PERMANENTEMENTE

Product form

ProductForm includes:

Basic information

Pricing

Classification

Features

Status

Components

ProductForm

import ProductForm from '@/app/components/admin/product-form' // Create new <ProductForm mode="create" /> // Edit existing <ProductForm mode="edit" product={product} />

ProductActions

import ProductActions from '@/app/components/admin/product-actions' <ProductActions productId={product._supabaseId} productName={product.name} isActive={product.isActive} />

Available actions:

Store page

The public store renders only active products:

// src/app/[locale]/(pages)/loja/page.tsx import { getAllProducts } from '@/app/server/products/get-all-products' import { TemplatesGrid } from '@/app/components/templates/TemplatesGrid' export default async function LojaPage() { const products = await getAllProducts() return ( <TemplatesGrid templates={products} /> ) }

Characteristics:

Image upload

Uploads use Supabase Storage:

// Bucket: products // Path: /products/{slug}/{filename} const { data, error } = await supabase.storage .from('products') .upload(`${slug}/${filename}`, file, { cacheControl: '3600', upsert: true, })

Configure the bucket via the Supabase Dashboard or SQL:

insert into storage.buckets (id, name, public) values ('products', 'products', true); create policy "Anyone can view product images" on storage.objects for select using (bucket_id = 'products'); create policy "Admins can upload product images" on storage.objects for insert to authenticated with check (bucket_id = 'products');

Categories and filters

Available categories

const categories = [ 'landing-page', 'dashboard', 'e-commerce', 'portfolio', 'blog', 'saas', 'mobile-app', 'book', ]

Stacks

const stacks = [ 'nextjs', 'react', 'vue', 'angular', 'svelte', 'tailwind', ]

Levels

const levels = [ 'beginner', 'intermediate', 'advanced', ]

Validation checklist

Before publishing a product:

Troubleshooting

Product does not appear in the store

Error while creating product

Image does not load

Stripe Price ID is invalid

Resources