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.
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 buttonsinterface 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)
}http://localhost:3000/en/admin/productsAccess requires authentication. Configure authorized emails in .env:
ADMIN_EMAILS=admin@example.com,outro@example.comimport { getAllProductsAdmin } from '@/app/server/products/get-all-products-admin'
// Returns every product, including inactive ones
const products = await getAllProductsAdmin()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)
}import { updateProduct } from '@/app/actions/products/update-product'
const result = await updateProduct(supabaseId, {
price: 59.99,
isPopular: true,
})| Type | Action | Reversible | When to use |
|---|---|---|---|
| Soft Delete | toggleProductActive | Yes | Temporarily disable |
| Hard Delete | deleteProduct | No | Remove 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 PERMANENTEMENTEProductForm includes:
import ProductForm from '@/app/components/admin/product-form'
// Create new
<ProductForm mode="create" />
// Edit existing
<ProductForm mode="edit" product={product} />import ProductActions from '@/app/components/admin/product-actions'
<ProductActions
productId={product._supabaseId}
productName={product.name}
isActive={product.isActive}
/>Available actions:
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:
isActive: trueUploads 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');const categories = [
'landing-page',
'dashboard',
'e-commerce',
'portfolio',
'blog',
'saas',
'mobile-app',
'book',
]const stacks = [
'nextjs',
'react',
'vue',
'angular',
'svelte',
'tailwind',
]const levels = [
'beginner',
'intermediate',
'advanced',
]Before publishing a product:
isActive: truerm -rf .next && npm run build