diff --git a/apps/openbaar/eslint.config.mjs b/apps/openbaar/eslint.config.mjs
new file mode 100644
index 0000000..af5ff32
--- /dev/null
+++ b/apps/openbaar/eslint.config.mjs
@@ -0,0 +1,34 @@
+import nx from '@nx/eslint-plugin';
+import baseConfig from '../../eslint.config.mjs';
+
+export default [
+ ...nx.configs['flat/angular'],
+ ...nx.configs['flat/angular-template'],
+ ...baseConfig,
+ {
+ files: ['**/*.ts'],
+ rules: {
+ '@angular-eslint/directive-selector': [
+ 'error',
+ {
+ type: 'attribute',
+ prefix: 'app',
+ style: 'camelCase',
+ },
+ ],
+ '@angular-eslint/component-selector': [
+ 'error',
+ {
+ type: 'element',
+ prefix: 'app',
+ style: 'kebab-case',
+ },
+ ],
+ },
+ },
+ {
+ files: ['**/*.html'],
+ // Override or add rules here
+ rules: {},
+ },
+];
diff --git a/apps/openbaar/project.json b/apps/openbaar/project.json
new file mode 100644
index 0000000..a85dfbd
--- /dev/null
+++ b/apps/openbaar/project.json
@@ -0,0 +1,80 @@
+{
+ "name": "openbaar",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "prefix": "app",
+ "sourceRoot": "apps/openbaar/src",
+ "tags": [],
+ "targets": {
+ "build": {
+ "executor": "@angular/build:application",
+ "outputs": ["{options.outputPath}"],
+ "defaultConfiguration": "production",
+ "options": {
+ "outputPath": "dist/apps/openbaar",
+ "browser": "apps/openbaar/src/main.ts",
+ "tsConfig": "apps/openbaar/tsconfig.app.json",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "apps/openbaar/public"
+ }
+ ],
+ "styles": ["apps/openbaar/src/styles.css"]
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "1mb",
+ "maximumError": "2mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "4kb",
+ "maximumError": "8kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ }
+ },
+ "serve": {
+ "continuous": true,
+ "executor": "@angular/build:dev-server",
+ "defaultConfiguration": "development",
+ "configurations": {
+ "production": {
+ "buildTarget": "openbaar:build:production"
+ },
+ "development": {
+ "buildTarget": "openbaar:build:development"
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint"
+ },
+ "test": {
+ "executor": "@angular/build:unit-test",
+ "options": {
+ "watch": false
+ }
+ },
+ "serve-static": {
+ "continuous": true,
+ "executor": "@nx/web:file-server",
+ "options": {
+ "buildTarget": "openbaar:build",
+ "staticFilePath": "dist/apps/openbaar/browser",
+ "spa": true
+ }
+ }
+ }
+}
diff --git a/apps/openbaar/public/favicon.ico b/apps/openbaar/public/favicon.ico
new file mode 100644
index 0000000..317ebcb
Binary files /dev/null and b/apps/openbaar/public/favicon.ico differ
diff --git a/apps/openbaar/src/app/app.config.ts b/apps/openbaar/src/app/app.config.ts
new file mode 100644
index 0000000..beb714e
--- /dev/null
+++ b/apps/openbaar/src/app/app.config.ts
@@ -0,0 +1,19 @@
+import { provideHttpClient } from '@angular/common/http';
+import {
+ ApplicationConfig,
+ provideBrowserGlobalErrorListeners,
+} from '@angular/core';
+import { provideRouter } from '@angular/router';
+import { appRoutes } from './app.routes';
+
+/**
+ * The openbaar register is a public, anonymous read: no DigiD, no auth interceptor. The app is served
+ * same-origin as the BFF (nginx proxies /openbaar), so the api-client's relative calls stay same-origin.
+ */
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideBrowserGlobalErrorListeners(),
+ provideRouter(appRoutes),
+ provideHttpClient(),
+ ],
+};
diff --git a/apps/openbaar/src/app/app.css b/apps/openbaar/src/app/app.css
new file mode 100644
index 0000000..e69de29
diff --git a/apps/openbaar/src/app/app.html b/apps/openbaar/src/app/app.html
new file mode 100644
index 0000000..0680b43
--- /dev/null
+++ b/apps/openbaar/src/app/app.html
@@ -0,0 +1 @@
+
diff --git a/apps/openbaar/src/app/app.routes.ts b/apps/openbaar/src/app/app.routes.ts
new file mode 100644
index 0000000..12dc5ef
--- /dev/null
+++ b/apps/openbaar/src/app/app.routes.ts
@@ -0,0 +1,4 @@
+import { Route } from '@angular/router';
+import { RegisterPage } from './register/register-page';
+
+export const appRoutes: Route[] = [{ path: '', component: RegisterPage }];
diff --git a/apps/openbaar/src/app/app.ts b/apps/openbaar/src/app/app.ts
new file mode 100644
index 0000000..ba60373
--- /dev/null
+++ b/apps/openbaar/src/app/app.ts
@@ -0,0 +1,12 @@
+import { Component } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+@Component({
+ imports: [RouterModule],
+ selector: 'app-root',
+ templateUrl: './app.html',
+ styleUrl: './app.css',
+})
+export class App {
+ protected title = 'openbaar';
+}
diff --git a/apps/openbaar/src/app/register/register-page.html b/apps/openbaar/src/app/register/register-page.html
new file mode 100644
index 0000000..c6745e4
--- /dev/null
+++ b/apps/openbaar/src/app/register/register-page.html
@@ -0,0 +1,5 @@
+
+
+ Openbaar BIG-register
+
+
diff --git a/apps/openbaar/src/app/register/register-page.spec.ts b/apps/openbaar/src/app/register/register-page.spec.ts
new file mode 100644
index 0000000..d9dd6c9
--- /dev/null
+++ b/apps/openbaar/src/app/register/register-page.spec.ts
@@ -0,0 +1,57 @@
+import { fireEvent, render, screen } from '@testing-library/angular';
+import { of } from 'rxjs';
+import { BffApiV1Service, type OpenbaarEntry } from 'api-client';
+import { axe } from 'vitest-axe';
+import { RegisterPage } from './register-page';
+
+const sample: OpenbaarEntry[] = [
+ { id: 'zaak-abc', status: 'INGEDIEND' },
+ { id: 'zaak-def', status: 'INGESCHREVEN' },
+];
+
+function providers(get = vi.fn().mockReturnValue(of(sample))) {
+ return {
+ get,
+ providers: [{ provide: BffApiV1Service, useValue: { getOpenbaarRegister: get } }],
+ };
+}
+
+describe('RegisterPage', () => {
+ it('lists the public register entries from the BFF on open', async () => {
+ const { get } = providers();
+ await render(RegisterPage, { providers: providers(get).providers });
+
+ expect(get).toHaveBeenCalled();
+ expect(await screen.findByText(/zaak-abc/)).toBeTruthy();
+ expect(screen.getByText(/INGEDIEND/)).toBeTruthy();
+ expect(screen.getByText(/zaak-def/)).toBeTruthy();
+ });
+
+ it('searches by the entered term', async () => {
+ const get = vi.fn().mockReturnValue(of(sample));
+ await render(RegisterPage, { providers: providers(get).providers });
+
+ fireEvent.input(screen.getByRole('searchbox'), { target: { value: 'zaak-abc' } });
+ fireEvent.click(screen.getByRole('button', { name: /zoek/i }));
+
+ expect(get).toHaveBeenLastCalledWith({ q: 'zaak-abc' });
+ });
+
+ it('shows an empty-state message when the register has no matches', async () => {
+ const get = vi.fn().mockReturnValue(of([] as OpenbaarEntry[]));
+ await render(RegisterPage, { providers: providers(get).providers });
+
+ expect(await screen.findByText(/geen inschrijvingen gevonden/i)).toBeTruthy();
+ });
+
+ it('has no WCAG 2.1 AA violations', async () => {
+ document.documentElement.lang = 'nl';
+ const { container } = await render(RegisterPage, { providers: providers().providers });
+
+ const results = await axe(container, {
+ runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] },
+ });
+
+ expect(results.violations).toEqual([]);
+ });
+});
diff --git a/apps/openbaar/src/app/register/register-page.ts b/apps/openbaar/src/app/register/register-page.ts
new file mode 100644
index 0000000..83276af
--- /dev/null
+++ b/apps/openbaar/src/app/register/register-page.ts
@@ -0,0 +1,27 @@
+import { Component, inject, signal } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { BffApiV1Service, type OpenbaarEntry } from 'api-client';
+import { UtrechtComponentsModule } from 'ui';
+
+/**
+ * The openbaar (public) BIG-register: an anonymous search over the read projection's public-safe
+ * view (id + status only — bsn/naam never leave the BFF; ADR-0010). Loads the full register on open
+ * and filters by the search term via the BFF's `/openbaar/register?q=` endpoint (S-09).
+ */
+@Component({
+ selector: 'app-register-page',
+ imports: [FormsModule, UtrechtComponentsModule],
+ templateUrl: './register-page.html',
+})
+export class RegisterPage {
+ private readonly bff = inject(BffApiV1Service);
+
+ protected readonly query = signal('');
+ protected readonly entries = signal([]);
+ protected readonly loading = signal(false);
+ protected readonly searched = signal(false);
+
+ search(): void {
+ // TODO(#10): query the BFF and render the results.
+ }
+}
diff --git a/apps/openbaar/src/index.html b/apps/openbaar/src/index.html
new file mode 100644
index 0000000..279e9b3
--- /dev/null
+++ b/apps/openbaar/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Openbaar BIG-register
+
+
+
+
+
+
+
+
diff --git a/apps/openbaar/src/main.ts b/apps/openbaar/src/main.ts
new file mode 100644
index 0000000..4258790
--- /dev/null
+++ b/apps/openbaar/src/main.ts
@@ -0,0 +1,6 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { App } from './app/app';
+import { appConfig } from './app/app.config';
+
+// The openbaar register is anonymous (no DigiD, no runtime config) — bootstrap directly.
+bootstrapApplication(App, appConfig).catch((err) => console.error(err));
diff --git a/apps/openbaar/src/styles.css b/apps/openbaar/src/styles.css
new file mode 100644
index 0000000..ade77c5
--- /dev/null
+++ b/apps/openbaar/src/styles.css
@@ -0,0 +1,2 @@
+/* NL Design System theme — Utrecht design tokens (docs/frontend-decisions.md). */
+@import '@utrecht/design-tokens/dist/index.css';
diff --git a/apps/openbaar/tsconfig.app.json b/apps/openbaar/tsconfig.app.json
new file mode 100644
index 0000000..a75ddab
--- /dev/null
+++ b/apps/openbaar/tsconfig.app.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": []
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
+}
diff --git a/apps/openbaar/tsconfig.json b/apps/openbaar/tsconfig.json
new file mode 100644
index 0000000..bb7614f
--- /dev/null
+++ b/apps/openbaar/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "isolatedModules": true,
+ "target": "es2022",
+ "moduleResolution": "bundler",
+ "emitDecoratorMetadata": false,
+ "module": "preserve"
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ]
+}
diff --git a/apps/openbaar/tsconfig.spec.json b/apps/openbaar/tsconfig.spec.json
new file mode 100644
index 0000000..2d36c49
--- /dev/null
+++ b/apps/openbaar/tsconfig.spec.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": ["vitest/globals"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.d.ts"]
+}