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 whitelistedsortBykey into a directionalorderByexpressionpaginationOf(total, limit, offset, itemsLength)builds thepaginationblock of aPaginatedResult
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 assignmentfacetService— Facets and facet values (filterable attributes)collectionService— Product collections (rule-based)taxService— Tax rate lookups and calculationspaymentService— Payment provider orchestrationshippingService— Shipping provider orchestrationpromotionService— Discount and promotion rulesreservationService— Stock reservationscustomerGroupService— Customer group managementcontentPageService— CMS content pagestranslationService— Multi-language translations for all entitiesassetService— Image/file managementrelatedProductService— Related-product linksdigitalDeliveryService— Delivery of digital products after paymentreviewService— 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.