Skip to Content

Services

All business logic lives in src/lib/server/services/. Each service is a singleton class.

Services import db from the database seam (src/lib/server/db/index.ts) rather than opening a connection themselves. The seam resolves to a local better-sqlite3 database on the Node.js target and to the D1 binding on Cloudflare, so the same service code runs on both targets. Multi-statement writes go through the seam’s atomic() helper. See Database.

Product Service

Manages products and their variants.

import { productService } from "$lib/server/services/products"; // Get by ID or slug — returns resolved flat fields const product = await productService.getById(123); const product = await productService.getBySlug("blue-shirt"); product.name; // "Blue Shirt" (default language, directly from products table) // List with pagination const { items, pagination } = await productService.list({ limit: 20 }); // Search const results = await productService.list({ search: "shirt" }); // CRUD await productService.create({ slug: "new-product" /* ... */ }); await productService.update(123, { /* ... */ }); await productService.delete(123);

Variant operations (stock, pricing, SKU management) are also handled through productService.

Order Service

Manages the full order lifecycle, including cart operations.

import { orderService } from "$lib/server/services/orders"; // The cart is a cookie — an order row is created only when checkout starts, // snapshotting the cookie lines into order lines with 15-min stock reservations. const { order, checkoutToken } = await orderService.startCheckout(cartLines, { customerId, checkoutToken }); // State transitions (all go through transitionState) await orderService.transitionState(orderId, "payment_pending"); await orderService.transitionState(orderId, "paid"); await orderService.transitionState(orderId, "shipped"); await orderService.transitionState(orderId, "delivered"); await orderService.transitionState(orderId, "cancelled"); // Checkout await orderService.setShippingAddress(orderId, address); await orderService.setCustomerEmail(orderId, email); await orderService.updateTotals(orderId); // Query const order = await orderService.getByCode("ORD-EJXEDYIX");

Customer Service

import { customerService } from "$lib/server/services/customers"; // Query const customer = await customerService.getById(1); const customer = await customerService.getByEmail("user@example.com");

The customer record for a signed-in user is created by the customer sync in src/hooks.server.ts, not by a service method. See Authentication.

Translation Service

Manages translations for all 8 translatable entities. Uses Drizzle’s onConflictDoUpdate for upserts.

import { translationService } from "$lib/server/services/translations"; // Get translations for an entity const rows = await translationService.getProductTranslations(productId); // Upsert a translation (insert or update) await translationService.upsertProductTranslation(productId, "fi", { name: "Sininen paita", slug: "sininen-paita", description: "<p>Hieno paita</p>" }); // Bulk methods for list pages const allCategoryTranslations = await translationService.getAllCategoryTranslations(); const facetValueTranslations = await translationService.getAllFacetValueTranslations(facetId);

See Localization for the full API and admin UI integration.

Pagination helpers

Paginated list methods share two helpers from src/lib/server/pagination.ts instead of repeating the sort/paging boilerplate per service:

  • resolveSort(sortColumnMap, sortBy, sortOrder, fallback?) resolves a whitelisted sortBy key into a directional orderBy expression
  • paginationOf(total, limit, offset, itemsLength) builds the pagination block of a PaginatedResult

parsePaginationParams(url) reads page, search, sort, and order from the query string for admin list pages.

Other Services

See src/lib/server/services/ for all available services including:

  • categoryService — Category tree, breadcrumbs, and product assignment
  • facetService — Facets and facet values (filterable attributes)
  • collectionService — Product collections (rule-based)
  • taxService — Tax rate lookups and calculations
  • paymentService — Payment provider orchestration
  • shippingService — Shipping provider orchestration
  • promotionService — Discount and promotion rules
  • reservationService — Stock reservations
  • customerGroupService — Customer group management
  • contentPageService — CMS content pages
  • translationService — Multi-language translations for all entities
  • assetService — Image/file management
  • relatedProductService — Related-product links
  • digitalDeliveryService — Delivery of digital products after payment
  • reviewService — Product reviews

Product search and reindexing live in product-search.ts, exposed as functions (listProducts, quickSearchProducts, reindexProduct, reindexAll, …) rather than a class.

The cart is a cookie, not a service singleton, and the wishlist is likewise just a cookie of product ids — there is no wishlistService. See Cart and Wishlists.

Promotion math

The discount formula lives in a single function, calculateDiscount ($lib/promotion-utils.ts, re-exported for services via services/promotion-utils.ts). It is shared by storefront price display and server-side order math, so the shown discount can’t drift from the charged one. Per-order application (coupon codes, automatic promotions, totals recalculation) routes through the private OrderService.computePromotionDiscount. See Promotions.

Emitting events

Services and routes record out-of-band work by calling emitEvent(type, payload) (a durable outbox insert) rather than doing it inline — e.g. the checkout completion emits order.paid. A scheduled drain processes these with retry. See Integrations.

MCP server

Hoikka ships a local MCP server (pnpm mcp, defined in scripts/mcp.ts and registered in .mcp.json) that exposes schema, catalog, and order tools to AI assistants over stdio. Point an MCP-capable client at it to let an agent inspect the schema and query products and orders directly.

Last updated on