Guidelines for AI coding agents working in the OpenStock codebase.
Self-hosted inventory and stock management system:
- Framework: Nuxt 4 with Vue 3 Composition API
- Database: Cloudflare D1 (SQLite) with Drizzle ORM
- Styling: TailwindCSS
- Hosting: Cloudflare Pages via NuxtHub
# Development
npm run dev # Start dev server on http://localhost:3000
# Build
npm run build # Production build
npm run preview # Preview production build locally
# Database
npm run db:generate # Generate migrations from schema changes
npm run db:populate # Populate database with sample data
npm run db:export # Export database
npm run db:sync # Export and push to remote
# Deployment
npm run deploy:cf # Build and deploy to Cloudflare Pages
npm run postinstall # nuxt prepare (run after npm install)Note: No test framework or linter is currently configured.
├── app/ # Frontend (Nuxt 4 app directory)
│ ├── components/ # Vue components (ui/, charts/)
│ ├── composables/ # Vue composables (useAuth, useToast, useSettings)
│ ├── layouts/ # Page layouts
│ ├── pages/ # File-based routing
│ └── middleware/ # Route middleware
├── server/ # Backend (Nitro)
│ ├── api/ # API endpoints (file-based routing)
│ ├── database/ # Drizzle schema (schema.ts)
│ ├── middleware/ # Server middleware (auth.ts)
│ └── utils/ # Server utilities (db.ts, id.ts)
├── migrations/ # SQL migration files
└── nuxt.config.ts # Nuxt configuration
// 1. External packages first
import { eq, desc } from 'drizzle-orm';
// 2. Server schema imports using the ~ alias
import { products } from '~~/server/database/schema';
import type { Product, Category } from '~~/server/database/schema';
// Nuxt auto-imports (no explicit import needed): useDB, tables, defineEventHandler, ref, reactive, computed, useFetch- Use TypeScript for all new files
- Prefer type inference; explicit types for API responses and public interfaces
- Use
interfacefor object shapes,typefor unions - Schema types from Drizzle:
export type Product = typeof products.$inferSelect;
<script setup lang="ts">
interface Props {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
});
const emit = defineEmits<{
(e: 'update', value: string): void;
}>();
</script>| Type | Convention | Example |
|---|---|---|
| Components | PascalCase.vue | AppHeader.vue |
| Composables | camelCase.ts, use-prefix | useAuth.ts |
| API routes | kebab-case | index.get.ts, [id].put.ts |
| Variables/functions | camelCase | const products = ref([]) |
| Database tables | camelCase (plural) | products, stockMovements |
| IDs | prefix_uuid | prod_abc123, var_def456 |
File naming: index.get.ts, index.post.ts, [id].get.ts, [id].put.ts, [id].delete.ts
export default defineEventHandler(async (event) => {
const db = useDB();
const id = getRouterParam(event, 'id');
const body = await readBody(event);
if (!id) {
throw createError({ statusCode: 400, message: 'ID is required' });
}
const item = await db.query.products.findFirst({
where: eq(tables.products.id, id),
});
if (!item) {
throw createError({ statusCode: 404, message: 'Item not found' });
}
return item;
});// API errors
throw createError({ statusCode: 400, message: 'Invalid input' });
throw createError({ statusCode: 401, message: 'Authentication required' });
throw createError({ statusCode: 403, message: 'Access denied' });
throw createError({ statusCode: 404, message: 'Resource not found' });
// Vue components
try {
await $fetch('/api/products', { method: 'POST', body: payload });
toast.success('Success', 'Product created');
} catch (error) {
console.error(error);
toast.error('Error', 'Failed to save product');
} finally {
isSubmitting.value = false;
}const db = useDB();
// Query with relations
const products = await db.query.products.findMany({
orderBy: [desc(tables.products.createdAt)],
with: { category: true, supplier: true, variants: true },
});
// Insert
const id = generateId('prod');
await db.insert(tables.products).values({ id, name: 'Product' });
// Update
await db.update(tables.products)
.set({ stockQuantity: newStock, updatedAt: new Date() })
.where(eq(tables.products.id, id));- Use Tailwind utility classes directly in templates
- Group utilities: layout → spacing → typography → colors → effects
- Semantic colors:
primary-600,gray-100,red-50 - Responsive prefixes:
sm:,md:,lg:
<button class="inline-flex items-center justify-center rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700">
Save
</button>-
Nuxt auto-imports: No explicit imports for
ref,reactive,computed,useFetch,defineEventHandler,useDB,tables -
Database migrations: After modifying
server/database/schema.ts, runnpm run db:generate -
ID generation: Use
generateId('prefix')fromserver/utils/id.ts -
Toast notifications: Use
const toast = useToast()for user feedback -
Type imports: Use
import type { }for type-only imports -
Authentication: Session via
nuxt-auth-utils. Server middleware enforces role-based access (admin, member, viewer)