Remove Supabase
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 5m40s
CI / Migrationen auf leerer Datenbank (push) Has been cancelled

The database moved to a container of our own; the platform is gone.
This takes out what was left of it — and, where the leftovers were load
bearing, moves rather than deletes.

Moved, not deleted:

  supabase/migrations/  -> db/migrations/      the schema's source of truth
  supabase/build-org.ts -> scripts/build-org.ts
  lib/supabase/types.ts -> lib/types.ts        52 import sites repointed

The bookkeeping needed care. It lived in `supabase_migrations.schema_migrations`,
and simply renaming the schema would have left the runner facing an empty
table: it would have called all 67 migrations pending and replayed them
against a database that is long since current. So the runner now creates
`migrationen.schema_migrations` and, once, copies the old rows across —
guarded so a second run does nothing and a fresh database skips it entirely.
Only then does migration 20260907100000 drop the old schema.

Deleted: the CLI config, the seed, the historical schema/function dumps
(nothing read them), scripts/umzug-von-supabase.sh (the move is done), and
both Supabase packages plus the CLI. Nothing in the application imported
them — the build now succeeds with no environment variables at all, which
is the proof.

Integration tests: six of them signed in through Supabase Auth and asserted
against the anon key and the service role. That model is gone, so the tests
were not portable — they are deleted. session-context and
employee-status-filter already ran on pg and are untouched; om-reporting is
ported to a direct connection because it guards a real risk (the reporting
line rule exists twice, once in SQL and once in TypeScript).

CI: the integration job started a Supabase stack. It now runs a postgres
service, applies deploy/db-init and every migration to an empty database —
that was the valuable part, and it still holds — then checks that a second
run is a no-op, which is what proves the bookkeeping works.

Docs: security-review.md audited a service-role key, a cookie adapter and
auth.users, none of which exist. Restating findings about removed components
would suggest today's system had been reviewed; it has not. It now records
what was removed and says a fresh review is due. data-model.md was already
marked obsolete and described the pre-OM schema; azure-migration.md was a
plan for a route not taken. Both deleted.

Verified: npm ci, typecheck, lint, 445 tests, build — all clean without the
packages. Integration tests skip cleanly with no database. Migration SQL and
the runner are reviewed but NOT executed: no Docker here, and the old
instance no longer resolves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 10:43:22 +02:00
parent 5c310c3a58
commit b87c8ad64c
155 changed files with 386 additions and 4014 deletions

View File

@@ -1,10 +1,10 @@
import type { EmploymentStatus } from "./supabase/types";
import type { EmploymentStatus } from "./types";
// Langzeitabwesenheit.
//
// The database still stores the employment status as 'Karenz' — renaming an
// enum value would mean rewriting every stored function that spells it, for
// a label change (see supabase/migrations/*_absence_type.sql). So the value
// a label change (see db/migrations/*_absence_type.sql). So the value
// is mapped to its display name here, in the one place the UI reads it from.
// Nur echte Abwesenheiten. Eine Teilzeit ist keine: wer in Bildungs-,

View File

@@ -1,4 +1,4 @@
import type { EmploymentStatus, NoteCategory } from "./supabase/types";
import type { EmploymentStatus, NoteCategory } from "./types";
const AVATAR_PALETTE = [
"#d6046e",
@@ -40,7 +40,7 @@ export const CATEGORY_STYLES: Record<ColorCategory, string> = {
};
// Audit-log / activity-feed action -> badge color, per the action list in
// supabase/schema.sql's audit_log comment.
// dem Kommentar an audit_log in der ersten Migration.
const ACTION_CATEGORY: Record<string, ColorCategory> = {
Neueinstellung: "success",
Wiedereinstellung: "success",

View File

@@ -3,7 +3,7 @@ import { jsonArrayFrom, jsonObjectFrom, zeitstempel } from "./db/json";
import { besetzungenAbfrage, pickPlacements } from "./placement";
import { buildOrgMaps, orgMapsAbfragen, type OrgEb } from "./org";
import { offeneStellenAbfrage, resolveOpenPositions, type OffeneStelle } from "./positions";
import type { HistoryEventType } from "./supabase/types";
import type { HistoryEventType } from "./types";
import type { AnstehendArt } from "./dashboard-filter";
// Was die Übersichtsseite liest — in zwei Rundreisen statt in dreizehn.

View File

@@ -9,8 +9,8 @@ import type { Schema } from "./schema";
// ═══ Warum das keine gewöhnliche Datenbankschicht ist ═══
//
// Die Zugriffsrechte liegen in der Datenbank: 21 RLS-Policies rufen
// is_hr_user() auf, und das fragt seit der Umstellung nicht mehr Supabase,
// sondern `current_setting('app.user_id')` — eine Sitzungsvariable.
// is_hr_user() auf, und das liest
// `current_setting('app.user_id')` — eine Sitzungsvariable.
//
// Sitzungsvariablen hängen an der *Verbindung*, nicht an der Anfrage. Und
// Verbindungen kommen aus einem Pool. Wird die Variable ohne Transaktion

View File

@@ -7,7 +7,7 @@ import { Pool, types } from "pg";
// Typprüfer und keiner der Tests fangen konnte.
//
// Die alte API-Schicht lieferte JSON: ein `date` kam als "2026-08-03" an,
// ein `numeric` als Zahl. Genau so steht es in lib/supabase/types.ts, und
// ein `numeric` als Zahl. Genau so steht es in lib/types.ts, und
// darauf baut die gesamte Anwendung — Sortierungen mit localeCompare,
// Vergleiche wie `entry_date <= stichtag`, das Ableiten des Status.
//
@@ -88,7 +88,7 @@ export function getPool(): Pool {
statement_timeout: 20_000,
idle_in_transaction_session_timeout: 20_000,
connectionTimeoutMillis: 10_000,
// Verwaltete Anbieter (Azure, RDS, Supabase) verlangen TLS; lokal nicht.
// Verwaltete Anbieter (Azure, RDS) verlangen TLS; im eigenen Netz nicht.
ssl: process.env.DATABASE_SSL === "false" ? undefined : { rejectUnauthorized: false },
});

View File

@@ -1,6 +1,6 @@
import "server-only";
import { sql, withUser, type Tx } from "./index";
import type { Database } from "@/lib/supabase/types";
import type { Database } from "@/lib/types";
// Aufruf einer Datenbankfunktion.
//

View File

@@ -1,18 +1,17 @@
import type { ColumnType } from "kysely";
import type { Database } from "@/lib/supabase/types";
import type { Database } from "@/lib/types";
// Die Tabellenform für Kysely, abgeleitet aus der bestehenden
// Schemabeschreibung — nicht daneben gestellt.
//
// Zwei Beschreibungen desselben Schemas driften auseinander, und die eine
// hier ist bereits gegen die Migrationen abgesichert: `npm run types:check`
// vergleicht lib/supabase/types.ts Spalte für Spalte mit
// supabase/migrations/*.sql und schlägt in der CI fehl, wenn etwas fehlt.
// vergleicht lib/types.ts Spalte für Spalte mit
// db/migrations/*.sql und schlägt in der CI fehl, wenn etwas fehlt.
// Diese Ableitung erbt diese Absicherung.
//
// Die Datei heisst noch lib/supabase/types.ts, weil sie aus der Zeit stammt,
// als PostgREST der Zugriffsweg war. Sie beschreibt reines PostgreSQL und
// wird beim Entfernen der Supabase-Pakete lediglich umbenannt.
// Sie beschreibt reines PostgreSQL und heisst seit dem Entfernen der
// Plattform-Pakete lib/types.ts.
type Tables = Database["public"]["Tables"];

View File

@@ -1,4 +1,4 @@
import type { DienstwagenArt } from "./supabase/types";
import type { DienstwagenArt } from "./types";
// Wie ein Dienstwagen benannt wird.
//

View File

@@ -1,6 +1,6 @@
import type { Expression, ExpressionBuilder, SqlBool } from "kysely";
import type { Schema } from "./db/schema";
import type { EmploymentStatus } from "./supabase/types";
import type { EmploymentStatus } from "./types";
// The SQL counterpart of deriveStatusAsOf() in lib/reports.ts.
//

View File

@@ -1,4 +1,4 @@
import type { AuditChange, HistoryEventType } from "./supabase/types";
import type { AuditChange, HistoryEventType } from "./types";
// Welche Historieneinträge sich zurücknehmen lassen — und warum die übrigen
// nicht.

View File

@@ -2,7 +2,7 @@ import type { Tx } from "./db";
import { jsonArrayFrom, zeitstempel } from "./db/json";
import { fmtName } from "./format";
import type { OrgEb } from "./org";
import type { Database } from "./supabase/types";
import type { Database } from "./types";
export type OpenNote = Database["public"]["Tables"]["employee_notes"]["Row"] & {
employeeName: string;

View File

@@ -2,7 +2,7 @@ import type { ExpressionBuilder } from "kysely";
import type { Tx } from "./db";
import { jsonArrayFrom } from "./db/json";
import type { Schema } from "./db/schema";
import type { Database } from "./supabase/types";
import type { Database } from "./types";
/**
* Der Ausdrucksbauer einer Abfrage ohne eigene Tabelle (selectNoFrom) — das

View File

@@ -1,6 +1,6 @@
import { ABSENCE_TYPES } from "./absence";
import { parseIsoDateParam } from "./reports";
import type { Weekday } from "./supabase/types";
import type { Weekday } from "./types";
// Ein Verzeichnis aller Auswahlkriterien — für die Oberfläche, die Abfrage
// und den Export dasselbe.

View File

@@ -25,7 +25,7 @@ import type {
SourceType,
TeilzeitArt,
WorkerType,
} from "./supabase/types";
} from "./types";
// Shared by the Berichte page and /api/export/* so they can never drift on
// what "the current view" means — same filters, same stichtag/event-window

View File

@@ -1,5 +1,5 @@
import { fmtName, todayIso, yearsBetweenIso } from "./format";
import type { EmploymentStatus, HistoryEventType, Weekday } from "./supabase/types";
import type { EmploymentStatus, HistoryEventType, Weekday } from "./types";
export { todayIso };

View File

@@ -1,7 +1,7 @@
// Hand-written to match supabase/schema.sql + supabase/migrations/*.sql (no DB
// connection string available to run `supabase gen types typescript` in this
// environment — regenerate from the live project once you have the Supabase
// CLI linked).
// Von Hand gepflegt, passend zu db/migrations/*.sql.
//
// Erzeugt wird hier nichts: scripts/check-schema-types.mjs haelt die Datei
// Spalte fuer Spalte gegen die Migrationen und meldet jede Abweichung.
export type EmploymentStatus = "Aktiv" | "Karenz" | "Geplant" | "Ausgetreten";
export type EmploymentType = "Vollzeit" | "Teilzeit";
@@ -94,20 +94,15 @@ export type PendingChangeType =
| "reorg";
export type PendingChangeStatus = "pending" | "applied" | "cancelled";
// @supabase/postgrest-js requires every table/view to carry a Relationships
// array (used for typed embedded selects) — left empty since no code in this
// app relies on nested/embedded resource selects.
type NoRelationships = { Relationships: [] };
export type Database = {
public: {
Tables: {
locations: NoRelationships & {
locations: {
Row: { id: string; name: string; country: string };
Insert: { id?: string; name: string; country: string };
Update: Partial<{ id: string; name: string; country: string }>;
};
profiles: NoRelationships & {
profiles: {
Row: {
id: string;
email: string;
@@ -139,7 +134,7 @@ export type Database = {
updated_at: string;
}>;
};
employees: NoRelationships & {
employees: {
Row: {
id: string;
personnel_number: number;
@@ -258,7 +253,7 @@ export type Database = {
};
Update: Partial<Database["public"]["Tables"]["employees"]["Insert"]>;
};
employee_history: NoRelationships & {
employee_history: {
Row: {
id: string;
employee_id: string;
@@ -283,7 +278,7 @@ export type Database = {
};
Update: Partial<Database["public"]["Tables"]["employee_history"]["Insert"]>;
};
employee_dependents: NoRelationships & {
employee_dependents: {
Row: {
id: string;
employee_id: string;
@@ -306,7 +301,7 @@ export type Database = {
};
Update: Partial<Database["public"]["Tables"]["employee_dependents"]["Insert"]>;
};
employee_notes: NoRelationships & {
employee_notes: {
Row: {
id: string;
employee_id: string;
@@ -335,17 +330,17 @@ export type Database = {
};
Update: Partial<Database["public"]["Tables"]["employee_notes"]["Insert"]>;
};
hire_drafts: NoRelationships & {
hire_drafts: {
Row: { id: string; created_by: string | null; step: number; payload: Record<string, unknown>; updated_at: string };
Insert: { id?: string; created_by?: string | null; step?: number; payload: Record<string, unknown>; updated_at?: string };
Update: Partial<Database["public"]["Tables"]["hire_drafts"]["Insert"]>;
};
saved_reports: NoRelationships & {
saved_reports: {
Row: { id: string; created_by: string | null; name: string; config: Record<string, unknown>; created_at: string };
Insert: { id?: string; created_by?: string | null; name: string; config: Record<string, unknown>; created_at?: string };
Update: Partial<Database["public"]["Tables"]["saved_reports"]["Insert"]>;
};
audit_log: NoRelationships & {
audit_log: {
Row: {
id: string;
occurred_at: string;
@@ -376,7 +371,7 @@ export type Database = {
};
Update: Partial<Database["public"]["Tables"]["audit_log"]["Insert"]>;
};
pending_org_changes: NoRelationships & {
pending_org_changes: {
Row: {
id: string;
employee_id: string;
@@ -404,14 +399,6 @@ export type Database = {
// ── SAP-OM-Modell ──────────────────────────────────────────
// O: rekursiv über parent_id, unit_type ist nur ein Etikett.
org_units: {
Relationships: [
{
foreignKeyName: "org_units_parent_id_fkey";
columns: ["parent_id"];
referencedRelation: "org_units";
referencedColumns: ["id"];
},
];
Row: {
id: string;
org_number: string;
@@ -435,7 +422,7 @@ export type Database = {
Update: Partial<Database["public"]["Tables"]["org_units"]["Insert"]>;
};
// C: Katalog der Tätigkeiten.
jobs: NoRelationships & {
jobs: {
Row: { id: string; code: string; title: string; created_at: string };
Insert: { id?: string; code: string; title: string; created_at?: string };
Update: Partial<Database["public"]["Tables"]["jobs"]["Insert"]>;
@@ -443,20 +430,6 @@ export type Database = {
// S: Planstelle. Der Name om_positions stammt aus der Zeit, in der die
// alte positions-Tabelle noch danebenstand; sie ist inzwischen weg.
om_positions: {
Relationships: [
{
foreignKeyName: "om_positions_org_unit_id_fkey";
columns: ["org_unit_id"];
referencedRelation: "org_units";
referencedColumns: ["id"];
},
{
foreignKeyName: "om_positions_job_id_fkey";
columns: ["job_id"];
referencedRelation: "jobs";
referencedColumns: ["id"];
},
];
Row: {
id: string;
position_number: string;
@@ -481,7 +454,7 @@ export type Database = {
};
// Die Onboarding-Checkliste: je Person und Punkt eine Zeile. Welche
// Punkte es gibt, steht in lib/onboarding.ts — nicht hier.
onboarding_tasks: NoRelationships & {
onboarding_tasks: {
Row: {
id: string;
employee_id: string;
@@ -510,7 +483,7 @@ export type Database = {
};
// Die Offboarding-Checkliste — dieselbe Form wie onboarding_tasks, für
// den anderen Weg. Punkte in lib/offboarding.ts.
offboarding_tasks: NoRelationships & {
offboarding_tasks: {
Row: {
id: string;
employee_id: string;
@@ -539,7 +512,7 @@ export type Database = {
};
// Kostenstellen. Die Zuordnung hängt an der Planstelle, nicht an der
// Person: der Sitz kostet Geld, auch wenn niemand darauf sitzt.
cost_centers: NoRelationships & {
cost_centers: {
Row: {
id: string;
code: string;
@@ -561,7 +534,7 @@ export type Database = {
Update: Partial<Database["public"]["Tables"]["cost_centers"]["Insert"]>;
};
// A011: Planstelle kontiert auf Kostenstelle, zeitabhängig.
position_cost_centers: NoRelationships & {
position_cost_centers: {
Row: {
id: string;
position_id: string;
@@ -602,20 +575,6 @@ export type Database = {
// Beide Richtungen: über die Planstelle hängt die Verortung in der
// Organisation, über die Person die Verortung in der Akte. Die
// Einbettung erspart an einem Dutzend Stellen eine zweite Abfrage.
Relationships: [
{
foreignKeyName: "position_assignments_position_id_fkey";
columns: ["position_id"];
referencedRelation: "om_positions";
referencedColumns: ["id"];
},
{
foreignKeyName: "position_assignments_employee_id_fkey";
columns: ["employee_id"];
referencedRelation: "employees";
referencedColumns: ["id"];
},
];
};
};
Views: Record<string, never>;