Skip to content
Tikab's Toolkit

The building blocks

Every block is its own workspace package under packages/. The rule that keeps them reusable: packages are generic, the app owns the domain. A block knows how to store files, send email, run jobs — it knows nothing about projects or tasks.

Dependency graph

Loading diagram...

Note the direction: the app depends on the blocks; no block ever depends on the app. The database block sits at the center because four others persist through it.

Two patterns every block follows

The browser stub

Server blocks must never leak into the client bundle — they carry credentials and native dependencies. Every server block therefore ships a stub through the browser condition of its exports map. If client code imports the package by mistake, it throws immediately with a message that points the right way:

/**
 * Browser-side stub for @repo/storage. The real implementation talks to
 * MinIO / Azure Blob with credentials — server-only. Any client-side import
 * resolves to this stub, which throws if called. Server code hits the real
 * implementation because Vite picks the "node" export condition in SSR.
 */
function serverOnly(): never {
  throw new Error(
    "@repo/storage is server-only. Call it from a createServerFn handler, an API route, or a job.",
  );
}
 
export const extForImage = serverOnly;
export const uploadKey = serverOnly;
export const putObject = serverOnly;
export const getObject = serverOnly;
export const removeObject = serverOnly;
export const fileUrl = serverOnly;

String-free UI

UI packages do one of two things: handle layout, or render one specific component. They never contain business logic and they never contain user-facing strings — every label, empty state and date locale arrives as a prop. The consumer owns all text through its i18n layer (the example uses Paraglide JS, with Swedish as the base locale and English alongside). That is what makes a block reusable across products and languages: AdminShell, DataTable, EditSheet and OutboxLog render in whatever language the consumer hands them.

Graceful degradation

Optional infrastructure turns itself on through the environment. Without HATCHET_CLIENT_TOKEN, enqueues log-and-skip. Without SMTP_HOST, email stays in the outbox. Without an AI key, callers fall back to their stubs. The app always boots — a missing optional service degrades a feature, never the whole product.

The blocks at a glance

BlockOne-linerDjango parallel
dbDrizzle client + composable schema fragmentsORM + migrations
authBetter Auth + the session choke pointdjango.contrib.auth
storageOne file API, MinIO or Azure Blob behind itdjango-storages
mailerOutbox-always email, SMTP when configuredsend_mail
jobsHatchet wiring with an env kill-switchCelery
aiEnv-driven LLM adapter factory
auditExplicit change history that outlives its subjectssignals + auditlog
configRuntime settings with a TTL cacheconstance
admin-uiChangelist, selection bar, bulk-edit sheet, admin shellthe admin
import-exportTwo-phase CSV import (plan → preview → confirm)django-import-export
debugSWEREF 99 transforms + control points + Sweden mapGeoDjango (parts)
metricsprom-client registry + HTTP histogramdjango-prometheus
enterprise sign-inLDAP directory + OIDC SSO on the auth coreallauth + auth-ldap
realtimeServer-push over Centrifugo (WebSocket)Channels / Daphne
cacheValkey JSON cache + shared rate-limit storethe cache framework
ui, env, ldap, edge-ssoPrimitives and enterprise auth helpers