← Back to projects

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

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.

lib/oiko/selfService/invoices/listInvoices.ts
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
// 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

Framework
  • Next.js 15
  • React
  • TypeScript
Styling
  • Tailwind CSS
  • Headless UI
CMS & Data
  • DatoCMS
  • GraphQL
  • gql.tada
Testing
  • Vitest
  • vitest-axe
  • Playwright
  • Storybook
Libraries
  • Leaflet
  • ibantools
  • QRCode