-- nachklang_admin: identity, sessions and per-app permissions for every -- *.nachklang.art app. -- -- The better-auth tables below (user, session, account, verification, passkey, -- rateLimit) mirror what better-auth 1.7.2 derives from the configuration in -- src/models/admin/admin.auth.ts, including the `disabled` additionalField on -- `user` and the `rateLimit` table that rateLimit.storage='database' requires. -- On every better-auth upgrade: re-derive the table list, diff it against this -- file, and add a numbered migration - never edit this one in place. -- -- Table and column names are better-auth's own ("camel" casing, so `rateLimit` -- and `userId`). MariaDB on Linux compares table names case-sensitively, so the -- casing here is load-bearing. The two Nachklang-owned tables at the bottom use -- the snake_case convention of the rest of this repo's SQL. CREATE TABLE IF NOT EXISTS `user` ( `id` VARCHAR(36) NOT NULL, `name` VARCHAR(255) NOT NULL, `email` VARCHAR(255) NOT NULL, `emailVerified` TINYINT(1) NOT NULL DEFAULT 0, `image` TEXT DEFAULT NULL, `createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Nachklang addition, declared through better-auth's additionalFields so -- the adapter knows about it. Disabling also revokes the user's sessions. `disabled` TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `user_email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS `session` ( `id` VARCHAR(36) NOT NULL, `expiresAt` DATETIME NOT NULL, `token` VARCHAR(255) NOT NULL, `createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `ipAddress` VARCHAR(255) DEFAULT NULL, `userAgent` TEXT DEFAULT NULL, `userId` VARCHAR(36) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `session_token` (`token`), KEY `session_user` (`userId`), CONSTRAINT `session_user_fk` FOREIGN KEY (`userId`) REFERENCES `user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS `account` ( `id` VARCHAR(36) NOT NULL, -- 1.7 addition: distinguishes a local credential account -- ("local:credential") from an OAuth issuer. Written by better-auth. `issuer` VARCHAR(255) NOT NULL, `accountId` VARCHAR(255) NOT NULL, `providerId` VARCHAR(255) NOT NULL, `userId` VARCHAR(36) NOT NULL, `accessToken` TEXT DEFAULT NULL, `refreshToken` TEXT DEFAULT NULL, `idToken` TEXT DEFAULT NULL, `accessTokenExpiresAt` DATETIME DEFAULT NULL, `refreshTokenExpiresAt` DATETIME DEFAULT NULL, `scope` TEXT DEFAULT NULL, `password` TEXT DEFAULT NULL, `createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `account_user` (`userId`), KEY `account_provider` (`providerId`, `accountId`), CONSTRAINT `account_user_fk` FOREIGN KEY (`userId`) REFERENCES `user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Password-reset and e-mail-verification tokens. CREATE TABLE IF NOT EXISTS `verification` ( `id` VARCHAR(36) NOT NULL, `identifier` VARCHAR(255) NOT NULL, `value` TEXT NOT NULL, `expiresAt` DATETIME NOT NULL, `createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `verification_identifier` (`identifier`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS `passkey` ( `id` VARCHAR(36) NOT NULL, `name` VARCHAR(255) DEFAULT NULL, `publicKey` TEXT NOT NULL, `userId` VARCHAR(36) NOT NULL, `credentialID` VARCHAR(255) NOT NULL, `counter` INT NOT NULL DEFAULT 0, `deviceType` VARCHAR(255) NOT NULL, `backedUp` TINYINT(1) NOT NULL DEFAULT 0, `transports` VARCHAR(255) DEFAULT NULL, `createdAt` DATETIME DEFAULT CURRENT_TIMESTAMP, `aaguid` VARCHAR(255) DEFAULT NULL, PRIMARY KEY (`id`), KEY `passkey_user` (`userId`), KEY `passkey_credential` (`credentialID`), CONSTRAINT `passkey_user_fk` FOREIGN KEY (`userId`) REFERENCES `user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Required by rateLimit.storage = 'database' in admin.auth.ts. Passenger may -- run several API instances, and an in-memory limiter would give each of them -- its own budget. CREATE TABLE IF NOT EXISTS `rateLimit` ( `id` VARCHAR(36) NOT NULL, `key` VARCHAR(255) NOT NULL, `count` INT NOT NULL DEFAULT 0, -- Epoch milliseconds, not a DATETIME: better-auth stores a number here. `lastRequest` BIGINT NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `rate_limit_key` (`key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- --------------------------------------------------------------------------- -- Nachklang-owned tables -- --------------------------------------------------------------------------- -- Which apps a user may administer. `admin` is just another app: holding it is -- what lets someone manage users and invitations. A permission is (app, role); -- `access` is the only role today, and the key admits several per app so finer -- ones can be added by inserting rows rather than by migrating this table. CREATE TABLE IF NOT EXISTS `user_app_permissions` ( `user_id` VARCHAR(36) NOT NULL, `app` ENUM('calendar','feedback','tickets','admin') NOT NULL, -- One row per (user, app, role). `access` means "may use this app at all" -- and is the only role today; the key allows several per app so a finer -- permission can be added later by inserting rows, not by migrating. `role` VARCHAR(32) NOT NULL DEFAULT 'access', `granted_by` VARCHAR(36) DEFAULT NULL, `granted_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, -- (user_id, app) is the leftmost prefix of this key, so the per-request -- permission lookup needs no separate index. PRIMARY KEY (`user_id`, `app`, `role`), CONSTRAINT `uap_user_fk` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- The only route to a new account: there is no public sign-up. Only the -- SHA-256 of the token is stored, so a dump of this table hands out no access. CREATE TABLE IF NOT EXISTS `invitations` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `email` VARCHAR(255) NOT NULL, `name` VARCHAR(255) NOT NULL, `token_hash` CHAR(64) NOT NULL, `permissions` JSON NOT NULL, `invited_by` VARCHAR(36) DEFAULT NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `expires_at` DATETIME NOT NULL, `accepted_at` DATETIME DEFAULT NULL, `revoked_at` DATETIME DEFAULT NULL, UNIQUE KEY `inv_token_hash` (`token_hash`), KEY `inv_email` (`email`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;