German Fibre Internet Provider
Order Funnel & Self-Service Portal
2025 – present
Overview
A full-stack Next.js 15 application for a German home fibre internet provider, combining a multi-step order funnel with a customer self-service portal. The order funnel takes new customers from address lookup — checking whether fibre is available at their location — through plan selection, call number porting, IBAN entry, and final order submission. The self-service portal lets existing customers download invoices, manage their contracts, and send messages to support, all behind a token-authenticated session.
Architecture
Browser / CDN
└── Next.js middleware
├── CSP header injection (per-request, Node.js runtime)
└── Encrypted session cookie management
└── App Router — Server Components + Route Handlers
└── oiko/ — BFF Invocation Layer
├── auth · session · token refresh
├── invoices · contracts · orders
└── messaging · account management
│
├── Backend REST API
└── DatoCMS (GraphQL · gql.tada type-safe queries)Key Contributions
- Contributed to a typed invocation layer (BFF pattern) using Next.js Server Components, centralising all backend API calls — auth, session lifecycle, token refresh, order management, invoices, contracts, and secure messaging
- Worked on the CMS-driven page rendering system (DatoCMS + GraphQL via gql.tada) enabling fully type-safe queries and content-editor autonomy over page composition
- Contributed to Next.js middleware for per-request CSP header injection and encrypted session token management, intercepting every page request before it reaches the App Router
- Developed features across the multi-step order journey: product selection, address lookup with interactive fibre availability map (Leaflet), call number porting, IBAN validation (ibantools), email typo correction, and password strength scoring
- Extended the component test suite with automated accessibility coverage (vitest-axe); all UI components validated against WCAG 2.2 success criteria
- Wrote Playwright end-to-end tests covering critical order paths; maintained Storybook for isolated component development and documentation
Code Examples
BFF Invocation Layer — Why It Exists
Server Components could call the provider's backend API directly, but that would scatter token injection, HTTP construction, and error handling across every page. Instead, every backend endpoint lives as a typed async function in the oiko/ layer. It injects the bearer token, fires the request, validates the response shape, and throws a structured error on failure — Server Components just call one function and receive typed data. This example is the invoice list endpoint: the Server Component passes an access token and pagination params and gets back a validated, typed list.
import { executeUrlQueryWithToken } from "~/lib/oiko/query";
import { newErrorWithStatusCause } from "~/lib/error/statusCause";
import { validInvoicesList } from "./validation";
export const listInvoices = async (
accessToken: string,
offset: number,
limit: number,
) => {
const response = await executeUrlQueryWithToken(
"ses/v1/dms/invoices",
{ offset: offset.toString(), limit: limit.toString() },
"GET",
accessToken,
);
if (!response.ok) {
const bodyText = await response.text();
throw newErrorWithStatusCause(
`Could not retrieve invoices: ${response.status} ${response.statusText} - ${bodyText}`,
response.status,
);
}
const body = (await response.json()) as unknown;
return validInvoicesList(body);
};
export type InvoicesList = NonNullable<Awaited<ReturnType<typeof listInvoices>>>;
export type Invoice = InvoicesList["list"][number];Type-Safe GraphQL with gql.tada
DatoCMS exposes custom scalar types (ItemId, DateTime, JsonField, etc.) that a standard GraphQL schema can't map to TypeScript on its own. Rather than writing type assertions or running a codegen script, the project initialises gql.tada with the full DatoCMS introspection snapshot and explicit scalar mappings. Every query written with graphql() is then type-checked against the live schema at compile time — field names, arguments, and return shapes all resolved automatically. ResultOf<typeof allProductsQuery> gives the exact response type with no manual interface declarations.
// lib/datoCms/graphql/graphql.ts
import { initGraphQLTada } from "gql.tada";
import type { introspection } from "./graphql-env";
export const graphql = initGraphQLTada<{
introspection: introspection;
scalars: {
BooleanType: boolean;
CustomData: Record<string, string>;
Date: string;
DateTime: string;
FloatType: number;
IntType: number;
ItemId: string;
JsonField: unknown;
MetaTagAttributes: Record<string, string>;
UploadId: string;
};
}>();
export { readFragment, maskFragments } from "gql.tada";
export type { FragmentOf, ResultOf, VariablesOf } from "gql.tada";
// lib/datoCms/products/allProductsQuery.ts
import type { ResultOf, ExecuteQueryOptions } from "~/lib/datoCms/graphql";
import { graphql, executeQuery } from "~/lib/datoCms/graphql";
import { productFragment } from "./productFragment";
import { allProductsDataFromResponse } from "./allProductsDataFromResponse";
export const allProductsQuery = graphql(
`
query allProducts {
products: allProducts(first: 500) {
...ProductData
}
}
`,
[productFragment],
);
// Full response shape inferred from the query + fragments — no manual types needed.
export type AllProductsResponse = ResultOf<typeof allProductsQuery>;
export const getAllProducts = async (
executeQueryOptions: ExecuteQueryOptions<object>,
) => {
const response = await executeQuery(allProductsQuery, executeQueryOptions);
return allProductsDataFromResponse(response);
};Tech Stack
- Next.js 15
- React
- TypeScript
- Tailwind CSS
- Headless UI
- DatoCMS
- GraphQL
- gql.tada
- Vitest
- vitest-axe
- Playwright
- Storybook
- Leaflet
- ibantools
- QRCode