Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Testing
  2. Browser boundaries

Browser boundaries#

A browser boundary is the line between your logic and the outside world: localStorage, navigator, location, the network. Craft keeps direct access out of your services while still making each of those dependencies explicit in the graph — so a test can replace exactly them, and nothing else.

Use them when a service touches the platform. Then test with boundaryOnly: the whole application graph stays real and only the boundaries are mocked, which is what makes a passing test mean something.

Browser boundaries keep direct browser access out of your craftService implementations while still making those dependencies explicit in the service graph.

Every boundary on this page is backed by a global crafted service marked with browserBoundary: true.

WARNING

Some APIs are not documented here yet.

Import#

The main DSL exports are:

typescript
import {
  BrowserCrypto,
  BrowserDocument,
  BrowserHistory,
  BrowserLocation,
  BrowserNavigator,
  BrowserPerformance,
  BrowserWindow,
  Console,
  Cookies,
  LocalStorage,
  SessionStorage,
} from '@craft-ts/core';

When you need to derive methods for later reuse, each boundary also exposes the usual generated helpers:

typescript
import { ConsoleService, CONSOLE_SERVICE_META_DATA } from '@craft-ts/core';

The same pattern exists for the other boundaries:

  • LocalStorageService

  • SessionStorageService

  • CookiesService

  • BrowserLocationService

  • BrowserHistoryService

  • BrowserNavigatorService

  • BrowserPerformanceService

  • BrowserCryptoService

  • BrowserDocumentService

  • BrowserWindowService

Motivation#

Direct browser access inside a service hides dependencies inside business logic and makes tracking harder.

Browser boundaries solve that in two complementary ways:

  • use yield* X.method(...) when the browser interaction should happen directly inside the generator

  • use XService(...) when you want to derive bound browser helpers and reuse them inside returned callbacks

Mental Model#

There are two valid ways to use a browser boundary.

Direct DSL#

Use the DSL when the browser interaction belongs to the generator itself.

ts
import { Console, craftService, craftExpose } from '@craft-ts/core';

const { BootLogger } = craftService(
  { name: 'BootLogger', providedIn: 'global' },
  function* () {
    yield* Console.log('boot');
    yield* Console.info('config loaded');

    yield* craftExpose('ready', true);
  },
);

Derived Service Helper#

Use XService(...) when the browser method needs to stay callable later from a returned method.

ts
import { ConsoleService, craftService, craftExpose } from '@craft-ts/core';

const { AuditTrail } = craftService(
  { name: 'AuditTrail', providedIn: 'global' },
  function* () {
    const consoleService = yield* ConsoleService(
      undefined,
      ({ log, error }) => ({
        log,
        error,
      }),
    );

    yield* craftExpose('trackUserAction', (action: string) =>
      consoleService.log('user action', action));
    yield* craftExpose('trackFailure', (error: unknown) =>
      consoleService.error('unexpected failure', error));
  },
);

That second form is what preserves derivability while still tracking the browser dependency explicitly.

Core Examples#

Console#

typescript
yield * Console.log('my service run');
yield * Console.error('unexpected failure', error);

Local Storage#

typescript
yield * LocalStorage.setItem('token', token);

const persistedToken = yield * LocalStorage.getItem('token');
const entryCount = yield * LocalStorage.length();

Session Storage#

typescript
yield * SessionStorage.setItem('active-tab', 'settings');

const tab = yield * SessionStorage.getItem('active-tab');

Cookies#

typescript
yield *
  Cookies.set('session', sessionId, {
    path: '/',
    sameSite: 'strict',
  });

const session = yield * Cookies.get('session');
const hasSession = yield * Cookies.has('session');

Location#

typescript
const href = yield * BrowserLocation.href();
const pathname = yield * BrowserLocation.pathname();

yield * BrowserLocation.reload();

History#

typescript
yield * BrowserHistory.replaceState({ step: 2 }, '', '/checkout?step=2');

const state = yield * BrowserHistory.state();

Document#

typescript
yield * BrowserDocument.setTitle('Checkout');

const title = yield * BrowserDocument.title();

Window#

typescript
const width = yield * BrowserWindow.innerWidth();

yield * BrowserWindow.scrollTo(0, 0);
yield * BrowserWindow.alert('Cache cleared! The page will reload.');

const confirmed =
  yield * BrowserWindow.confirm('Cache cleared! The page will reload.');

if (confirmed) {
  yield * BrowserLocation.reload();
}

Performance And Crypto#

typescript
const now = yield * BrowserPerformance.now();
const uuid = yield * BrowserCrypto.randomUUID();

API Reference#

Every service below is:

  • providedIn: 'global'

  • browserBoundary: true

  • exposed both as a DSL object and as generated service helpers

Console and ConsoleService#

Methods:

  • debug

  • info

  • log

  • warn

  • error

  • trace

  • group

  • groupCollapsed

  • groupEnd

  • time

  • timeEnd

Generated helpers:

  • ConsoleService

  • ConsoleService

  • CONSOLE_SERVICE_META_DATA

LocalStorage and LocalStorageService#

Methods:

  • getItem

  • setItem

  • removeItem

  • clear

  • key

  • length

SessionStorage and SessionStorageService#

Methods:

  • getItem

  • setItem

  • removeItem

  • clear

  • key

  • length

Cookies and CookiesService#

Methods:

  • get

  • getAll

  • set

  • remove

  • has

BrowserLocation and BrowserLocationService#

Methods:

  • href

  • origin

  • protocol

  • host

  • hostname

  • port

  • pathname

  • search

  • hash

  • assign

  • replace

  • reload

BrowserHistory and BrowserHistoryService#

Methods:

  • length

  • state

  • back

  • forward

  • go

  • pushState

  • replaceState

BrowserNavigator and BrowserNavigatorService#

Methods:

  • userAgent

  • language

  • languages

  • onLine

  • cookieEnabled

  • sendBeacon

BrowserPerformance and BrowserPerformanceService#

Methods:

  • now

  • mark

  • measure

  • clearMarks

  • clearMeasures

BrowserCrypto and BrowserCryptoService#

Methods:

  • randomUUID

  • getRandomValues

  • digest

BrowserDocument and BrowserDocumentService#

Methods:

  • title

  • setTitle

  • lang

  • setLang

  • dir

  • setDir

  • visibilityState

  • hasFocus

BrowserWindow and BrowserWindowService#

Methods:

  • innerWidth

  • innerHeight

  • scrollX

  • scrollY

  • scrollTo

  • alert

  • confirm

Related Adapter: CraftHttpClient#

CraftHttpClient is implemented, but it is not a browser boundary.

Unlike Console, LocalStorage, or BrowserLocation, CraftHttpClient is a typed service boundary. It belongs in the dependency graph rather than being treated as a browser-global.

Its contract is intentionally different:

  • it is not treated as browserBoundary: true

  • it requires success: response<T>() inside a declarative builder

  • it can declare ordered exceptions: [function* (...) { ... }] rules

  • it returns a promise of Success | craftException({ _tag: 'HttpError' })

Usage looks like this:

typescript
const getUsers =
  yield *
  CraftHttpClient.get(({ response }) => ({
    url: '/api/users',
    params: { page: 1 },
    success: response<User[]>(),
  }));

const createUser =
  yield *
  CraftHttpClient.post(({ response }) => ({
    url: '/api/users',
    payload,
    success: response<User>(),
  }));

const login =
  yield *
  CraftHttpClient.post(({ response }) => ({
    url: '/api/login',
    payload,
    success: response<{ token: string }>(),
    exceptions: [
      function* ({ status, code, content }) {
        if (!(yield* status(400))) return;
        if (!(yield* code('PASSWORD_REQUIRED'))) return;
        if (!(yield* content('Password is required'))) return;

        return craftException({ _tag: 'PASSWORD_REQUIRED' });
      },
    ],
  }));

const users = await getUsers();
const createdUser = await createUser();
const loginResult = await login();

Design Constraints#

The browser boundaries stay intentionally narrow.

  • Reads are exposed as methods so the public API stays uniform with yield*.

  • Raw window, document, and DOM nodes are not exposed as public outputs.

  • BrowserDocument and BrowserWindow remain minimal rather than becoming generic escape hatches.

This keeps the API focused on explicit browser interactions instead of reintroducing broad direct access to host globals.

Relationship With craftService#

Browser boundaries participate in the same dependency tracking model as any other crafted service.

  • craftService is what you use to consume them and compose higher-level services.

  • Use a small craftService adapter for host dependencies that are not part of this built-in browser boundary set.

See Also#

  • craftService

  • Architecture rules — assert HTTP only crosses a boundary

←Type-level testsPage stability→

On this page

  • Import
  • Motivation
  • Mental Model
  • Direct DSL
  • Derived Service Helper
  • Core Examples
  • Console
  • Local Storage
  • Session Storage
  • Cookies
  • Location
  • History
  • Document
  • Window
  • Performance And Crypto
  • API Reference
  • Console and ConsoleService
  • LocalStorage and LocalStorageService
  • SessionStorage and SessionStorageService
  • Cookies and CookiesService
  • BrowserLocation and BrowserLocationService
  • BrowserHistory and BrowserHistoryService
  • BrowserNavigator and BrowserNavigatorService
  • BrowserPerformance and BrowserPerformanceService
  • BrowserCrypto and BrowserCryptoService
  • BrowserDocument and BrowserDocumentService
  • BrowserWindow and BrowserWindowService
  • Related Adapter: CraftHttpClient
  • Design Constraints
  • Relationship With craftService
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search