Products, inventory, customer assets, images, suppliers, and asset permission boundaries.
Assets is a tenant-scoped domain split into three related surfaces:
This split avoids a single overloaded assets.mode table. Shared product facts live on products; current stock state lives on inventory_stock_records; stock change history lives on inventory_movements; serial, assignment, and service lifecycle facts live on customer_assets.
| Table | Ownership | Purpose |
|---|---|---|
public.products | Tenant (organization_id) | Product/catalog definitions: name, brand, SKU/EAN, prices, VAT, status. |
public.inventory_locations | Tenant | Reusable stock locations for filtering, reporting, and future transfers. |
public.inventory_stock_records | Tenant | Current stock state for a product at a location: on-hand, reserved, reorder point, lot/serial, expiration. |
public.inventory_movements | Tenant | Append-only stock movement ledger for counts, adjustments, reservations, transfers, and write-offs. |
public.customer_assets | Tenant | Specific installed or maintained items linked to a product, customer, and optional customer location. |
public.product_categories | Tenant | Hierarchical product groupings for filtering and reporting. |
public.product_category_assignments | Tenant | Explicit many-to-many category assignments for products. |
public.asset_tags | Tenant | Labels shared by products and customer assets. |
public.asset_tag_assignments | Tenant | Many-to-many tag assignments; exactly one product or customer asset target. |
public.asset_images | Tenant | Ordered image metadata for products. |
public.customer_asset_files | Tenant | Uploaded files and image previews attached to customer assets. |
public.suppliers | Tenant | Organization-owned supplier masters (contact, address, commercial defaults, integration ids). |
public.product_supplier_links | Tenant | Per-product ordering details: SKU, MOQ, pack size, lead time, cost, preferred/active, external catalog id. |
public.asset_custom_attributes | Tenant | Typed custom attributes for product and customer asset records. |
source/external_id join keys. API credentials are never stored on suppliers.supplier_id and free-text reference_number (PO/ASN/delivery note) prepare purchase receipts and returns until purchase-order documents exist.quote_assets links remain a separate many-to-many for quotes that cover installed equipment.Historical documents and future work orders should snapshot commercial and assignment details at creation time rather than re-reading mutable product or customer asset rows on every render.
Products stay tenant-owned in v1. This is deliberate: organizations need their own names, pricing, VAT settings, supplier choices, images, and local overrides.
Product categories are tenant-owned and hierarchical through product_categories.parent_id. Products attach to categories through product_category_assignments, so assigning a product to a parent category never implicitly assigns it to subcategories. Filtering follows the same explicit rule: a parent filter matches only products directly assigned to that parent.
Future supplier API integration should follow the Google Calendar pattern, not Stripe Payments: a marketplace integration_definitions row per vendor, credentials and sync state in catalog-owned supplier_connections (or Vercel Connect), and an integration_installations row with domain_kind = supplier_connection. Tenant suppliers.source / suppliers.external_id point at that connection. product_supplier_links.external_id is the remote catalog product id. Do not store API credentials on suppliers, and do not make tenant product rows globally shared.
When a vendor lands, sync should import catalog records and propose or create product_supplier_links. Ordering (task material shortfall and later inventory reorder) should create a purchase request through the preferred link’s connection, falling back to mailto or the supplier page when no connection exists. Receiving stock later uses inventory_movements.purchase_receipt plus supplier_id / reference_number until dedicated purchase-order tables exist.
Permissions introduced with this slice:
organization.assets.vieworganization.assets.createorganization.assets.updateorganization.assets.deleteThey live in packages/identity/src/rbac/permissions.ts and are seeded through supabase/schemas/055_functions_rbac.sql. All asset tables enforce tenant access through public.has_org_permission(...) in supabase/schemas/073_rls_assets.sql.
The first implementation keeps one Assets permission set for Products, Inventory, and Customer Assets. Split permissions can be added later if the product needs different permissions for stock management versus customer asset management.
Catalog and customer-asset files live in feature-named public buckets. Paths are UUID-first (never org slugs). Organization letterhead images stay in the organization bucket, not here.
products/<orgId>/<productId>/<imageId>.webp
products/<orgId>/brands/<brandId>/<imageId>.webp
assets/<orgId>/<assetId>/<fileId>.<ext>
organization/<orgId>/logo/<version>.webp
organization/<orgId>/branding/<imageId>.webpOnly one normalized image version is persisted. asset_images.position controls ordering in the UI.
supplier_connections and the integrations marketplace.