Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ CLOUDFLARE_D1_TOKEN=""

# =============================================================================
# IDP 설정
# 초기 관리자 계정은 `bun run setup` 실행 시 D1에 직접 생성됩니다.
# 초기 관리자 계정은 `bun run setup` 실행 시 활성 DB(방언 무관)에 생성됩니다.
# 수동/CI 시드: IDP_BOOTSTRAP_ADMIN_USERNAME/EMAIL/PASSWORD(+NAME) 설정 후
# bun run db:seed (방언별: db:seed:pg 등). 비대화 환경은 SEED_RESET=0|1 로 초기화 지정.
# =============================================================================

# 기본 테넌트 이름
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
- name: Setup Wrangler config
env:
WRANGLER_JSONC: ${{ secrets.WRANGLER_JSONC }}
run: printf '%s' "$WRANGLER_JSONC" > wrangler.jsonc; mkdir -p static/__; printf '%s' "$WRANGLER_JSONC" > static/__/wrangler.jsonc.fuckyou;
run: printf '%s' "$WRANGLER_JSONC" > wrangler.jsonc

- name: Generate Wrangler types
run: bunx wrangler types
Expand All @@ -79,6 +79,9 @@ jobs:
- name: Build
run: bun run build
env:
# postgres 방언 선택 — vite/svelte config 가 빌드 시점에 스키마·드라이버를
# 선택하므로 반드시 빌드 env 로 지정해야 한다 (미설정 시 d1 로 번들링됨).
DB_DIALECT: postgres
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

Expand Down
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,3 @@ wrangler.jsonc
wrangler.*.jsonc
!wrangler.example.jsonc
worker-configuration.d.ts

# Drizzle generated files
drizzle/
35 changes: 26 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,21 @@ bun install
bun run setup
```

`bun run setup`은 아래 과정을 대화형으로 안내합니다:
`bun run setup`은 아래 과정을 대화형으로 안내합니다. DB 방언은 `--dialect`(`d1`(기본) | `postgres` | `mysql` | `sqlite`) 또는 `DB_DIALECT` 환경변수로 선택하며, 방언에 따라 3~5단계가 갈라집니다:

1. **wrangler 로그인 확인** — 미로그인 시 `wrangler login` 자동 실행
2. **설정 파일 생성** — `wrangler.example.jsonc` → `wrangler.jsonc`, `.env.example` → `.env`
3. **D1 데이터베이스** — 새로 생성하거나 기존 DB 선택 (프리뷰 DB 선택적)
4. **파일 업데이트** — DB ID 및 계정 ID를 `wrangler.jsonc`, `.env`에 자동 기입
5. **마이그레이션** — `bun run db:generate` 실행 후 D1에 스키마 적용 (충돌 테이블 감지 및 처리 포함)
6. **초기 관리자 계정 생성** — 조직명, 관리자 계정 정보, Issuer URL 입력 후 D1에 직접 삽입 (비밀번호 미입력 시 자동 생성)
7. **서명 키** — `IDP_SIGNING_KEY_SECRET` 자동 생성 또는 직접 입력 후 `.env`에 저장
1. **설정 파일 생성** — `wrangler.example.jsonc` → `wrangler.jsonc`, `.env.example` → `.env`
2. **DB 방언 결정** — `--dialect` > `DB_DIALECT` env > `.env` > 프롬프트
3. **DB 설정**
- `d1`: wrangler 로그인 확인 후 D1 새로 생성 또는 기존 DB 선택 (프리뷰 DB 선택적), DB ID·계정 ID를 `wrangler.jsonc`/`.env`에 자동 기입
- `postgres`/`mysql`/`sqlite`: `DATABASE_URL` 입력(또는 기존 값 재사용), postgres/mysql 은 Hyperdrive 구성 ID 입력 시 `wrangler.jsonc` 의 `HYPERDRIVE` 바인딩 자동 설정
4. **마이그레이션** — 방언별 `db:generate*` 실행 후 스키마 적용 (D1 은 충돌 테이블 감지 및 처리 포함)
5. **초기 관리자 계정 생성** — 조직명, 관리자 계정 정보, Issuer URL 입력 후 DB에 시드 (비밀번호 미입력 시 자동 생성)
6. **서명 키** — `IDP_SIGNING_KEY_SECRET` 자동 생성 또는 직접 입력 후 `.env`에 저장

```bash
# 예: PostgreSQL 로 셋업
bun run setup -- --dialect postgres --database-url "postgres://user:pass@host:5432/db" --hyperdrive-id <id>
```

> R2 버킷(`keystone-skin-cache`)은 커스텀 로그인 스킨 기능을 사용할 때 필요합니다. 사용하지 않는다면 `wrangler.jsonc`의 `r2_buckets` 항목을 주석 처리해도 됩니다.

Expand Down Expand Up @@ -202,7 +208,7 @@ wrangler secret put IDP_SIGNING_KEY_SECRET
| `CLOUDFLARE_D1_PREVIEW_DATABASE_ID` | 선택 | 프리뷰용 D1 데이터베이스 ID |
| `CLOUDFLARE_D1_TOKEN` | 선택 | D1 API 토큰 (`db:migrate` 스크립트에서 사용) |

> **참고**: 초기 관리자 계정은 `bun run setup` 실행 시 D1에 직접 생성됩니다. `IDP_BOOTSTRAP_ADMIN_*` 환경변수는 사용하지 않습니다.
> **참고**: 초기 관리자 계정은 `bun run setup` 이 생성합니다. 수동/CI 시드가 필요하면 `IDP_BOOTSTRAP_ADMIN_USERNAME` / `IDP_BOOTSTRAP_ADMIN_EMAIL` / `IDP_BOOTSTRAP_ADMIN_PASSWORD` (+선택 `IDP_BOOTSTRAP_ADMIN_NAME`) 를 설정하고 `bun run db:seed`(방언별: `db:seed:pg` 등)를 실행하세요. 비대화 환경에서는 `SEED_RESET=0|1` 로 초기화 여부를 지정합니다.

### Cloudflare 바인딩 (`wrangler.jsonc`)

Expand All @@ -228,9 +234,15 @@ wrangler secret put IDP_SIGNING_KEY_SECRET
| `bun run db:generate:sqlite` | Drizzle 마이그레이션 SQL 생성 (libSQL/SQLite) |
| `bun run db:generate:pg` | Drizzle 마이그레이션 SQL 생성 (PostgreSQL) |
| `bun run db:generate:mysql` | Drizzle 마이그레이션 SQL 생성 (MySQL) |
| `bun run db:migrate` | 마이그레이션 적용 + seed 마이그레이션 (D1) |
| `bun run db:migrate:pg` | 마이그레이션 적용 + seed 마이그레이션 (PostgreSQL) |
| `bun run db:seed` | 기본 데이터 시드 — 테넌트/관리자/서비스 role (D1) |
| `bun run db:seed:pg` | 기본 데이터 시드 (PostgreSQL) |
| `bun run db:studio` | Drizzle Studio 실행 (스키마/데이터 GUI) |
| `bun run deploy` | Cloudflare Workers 배포 |

> `db:migrate` / `db:seed` / `db:push` / `db:studio` 는 `:pg` / `:mysql` / `:sqlite` 접미사로 방언별 실행이 가능합니다. postgres/mysql/sqlite 는 `DATABASE_URL`, D1 은 `CLOUDFLARE_*` 환경변수를 사용합니다. `scripts/seed.ts` 와 `scripts/seed-migrate.ts` 는 방언 무관(공용 헬퍼 `scripts/lib/db.ts`)이며, D1 은 REST API 로 접근합니다.

## DB 방언 선택 (d1 / sqlite / postgresql / mysql)

D1(SQLite)·libSQL(SQLite)·PostgreSQL·MySQL 을 지원하며, **배포 단위로 하나만** 사용합니다. D1 은 편의성이 높지만 지연이 큰 편이라, 낮은 지연이 필요하면 Cloudflare Hyperdrive 로 PostgreSQL/MySQL 에 연결하거나 자체 DB 에 직결할 수 있습니다.
Expand Down Expand Up @@ -267,7 +279,12 @@ BUILD_TARGET=node DB_DIALECT=sqlite DATABASE_URL="file:./keystone.db" bun run bu
wrangler hyperdrive create keystone-pg --connection-string="postgres://user:pass@host:5432/db"
wrangler hyperdrive create keystone-mysql --connection-string="mysql://user:pass@host:3306/db"
# → 출력된 id 를 wrangler.jsonc 의 hyperdrive[].id 에 채우고 주석 해제
# (또는 bun run setup -- --dialect postgres --hyperdrive-id <id> 로 자동 설정)
# (Hyperdrive 없이 직결하려면 DATABASE_URL 을 var/secret 으로 설정)

# 스키마 적용 + 시드 (PostgreSQL 예시 — DATABASE_URL 사용)
bun run db:generate:pg && bun run db:migrate:pg
bun run db:seed:pg
```

### 배포 타깃: Cloudflare Workers vs 순수 Node
Expand Down
246 changes: 246 additions & 0 deletions drizzle/0000_lumpy_bloodstorm.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
CREATE TABLE `audit_events` (
`id` text PRIMARY KEY NOT NULL,
`tenant_id` text NOT NULL,
`user_id` text,
`actor_id` text,
`sp_or_client_id` text,
`kind` text NOT NULL,
`outcome` text NOT NULL,
`ip` text,
`user_agent` text,
`detail_json` text,
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
CREATE INDEX `audit_events_tenant_kind_idx` ON `audit_events` (`tenant_id`,`kind`);--> statement-breakpoint
CREATE INDEX `audit_events_tenant_created_idx` ON `audit_events` (`tenant_id`,`created_at`);--> statement-breakpoint
CREATE INDEX `audit_events_user_idx` ON `audit_events` (`user_id`);--> statement-breakpoint
CREATE TABLE `credentials` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`type` text NOT NULL,
`label` text,
`secret` text,
`public_key` text,
`credential_id` text,
`counter` integer DEFAULT 0 NOT NULL,
`transports` text,
`last_used_at` integer,
`used_at` integer,
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE INDEX `credentials_user_idx` ON `credentials` (`user_id`);--> statement-breakpoint
CREATE INDEX `credentials_user_type_idx` ON `credentials` (`user_id`,`type`);--> statement-breakpoint
CREATE UNIQUE INDEX `credentials_webauthn_credential_id_uidx` ON `credentials` (`credential_id`);--> statement-breakpoint
CREATE TABLE `identities` (
`id` text PRIMARY KEY NOT NULL,
`tenant_id` text NOT NULL,
`user_id` text NOT NULL,
`provider` text NOT NULL,
`subject` text NOT NULL,
`email` text,
`raw_profile_json` text,
`linked_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
`last_login_at` integer,
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `identities_tenant_provider_subject_uidx` ON `identities` (`tenant_id`,`provider`,`subject`);--> statement-breakpoint
CREATE INDEX `identities_user_idx` ON `identities` (`user_id`);--> statement-breakpoint
CREATE TABLE `identity_providers` (
`id` text PRIMARY KEY NOT NULL,
`tenant_id` text NOT NULL,
`kind` text NOT NULL,
`name` text NOT NULL,
`client_id` text,
`client_secret_enc` text,
`discovery_url` text,
`metadata_xml` text,
`scopes` text,
`config_json` text,
`enabled` integer DEFAULT false NOT NULL,
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE INDEX `idp_tenant_idx` ON `identity_providers` (`tenant_id`);--> statement-breakpoint
CREATE UNIQUE INDEX `idp_tenant_name_uidx` ON `identity_providers` (`tenant_id`,`name`);--> statement-breakpoint
CREATE TABLE `oidc_clients` (
`id` text PRIMARY KEY NOT NULL,
`tenant_id` text NOT NULL,
`client_id` text NOT NULL,
`client_secret_hash` text,
`name` text NOT NULL,
`redirect_uris` text NOT NULL,
`post_logout_redirect_uris` text,
`scopes` text DEFAULT 'openid' NOT NULL,
`grant_types` text DEFAULT 'authorization_code,refresh_token' NOT NULL,
`response_types` text DEFAULT 'code' NOT NULL,
`token_endpoint_auth_method` text DEFAULT 'client_secret_basic' NOT NULL,
`require_pkce` integer DEFAULT true NOT NULL,
`id_token_signed_response_alg` text DEFAULT 'RS256' NOT NULL,
`jwks_uri` text,
`jwks` text,
`enabled` integer DEFAULT true NOT NULL,
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `oidc_clients_tenant_client_id_uidx` ON `oidc_clients` (`tenant_id`,`client_id`);--> statement-breakpoint
CREATE INDEX `oidc_clients_tenant_idx` ON `oidc_clients` (`tenant_id`);--> statement-breakpoint
CREATE TABLE `oidc_grants` (
`id` text PRIMARY KEY NOT NULL,
`tenant_id` text NOT NULL,
`client_id` text NOT NULL,
`user_id` text NOT NULL,
`session_id` text,
`code` text NOT NULL,
`code_challenge` text,
`code_challenge_method` text,
`redirect_uri` text NOT NULL,
`scope` text NOT NULL,
`nonce` text,
`state` text,
`expires_at` integer NOT NULL,
`used_at` integer,
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`session_id`) REFERENCES `sessions`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
CREATE UNIQUE INDEX `oidc_grants_code_uidx` ON `oidc_grants` (`code`);--> statement-breakpoint
CREATE INDEX `oidc_grants_tenant_client_idx` ON `oidc_grants` (`tenant_id`,`client_id`);--> statement-breakpoint
CREATE INDEX `oidc_grants_expires_idx` ON `oidc_grants` (`expires_at`);--> statement-breakpoint
CREATE TABLE `oidc_refresh_tokens` (
`id` text PRIMARY KEY NOT NULL,
`tenant_id` text NOT NULL,
`client_id` text NOT NULL,
`user_id` text NOT NULL,
`session_id` text,
`token_hash` text NOT NULL,
`scope` text NOT NULL,
`expires_at` integer NOT NULL,
`revoked_at` integer,
`replaced_by_id` text,
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`session_id`) REFERENCES `sessions`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
CREATE UNIQUE INDEX `oidc_refresh_tokens_hash_uidx` ON `oidc_refresh_tokens` (`token_hash`);--> statement-breakpoint
CREATE INDEX `oidc_refresh_tokens_user_idx` ON `oidc_refresh_tokens` (`user_id`);--> statement-breakpoint
CREATE TABLE `saml_sessions` (
`id` text PRIMARY KEY NOT NULL,
`tenant_id` text NOT NULL,
`sp_id` text NOT NULL,
`user_id` text NOT NULL,
`session_id` text,
`session_index` text NOT NULL,
`name_id` text NOT NULL,
`name_id_format` text,
`not_on_or_after` integer NOT NULL,
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
`ended_at` integer,
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`sp_id`) REFERENCES `saml_sps`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`session_id`) REFERENCES `sessions`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
CREATE UNIQUE INDEX `saml_sessions_session_index_uidx` ON `saml_sessions` (`session_index`);--> statement-breakpoint
CREATE INDEX `saml_sessions_tenant_sp_idx` ON `saml_sessions` (`tenant_id`,`sp_id`);--> statement-breakpoint
CREATE TABLE `saml_sps` (
`id` text PRIMARY KEY NOT NULL,
`tenant_id` text NOT NULL,
`entity_id` text NOT NULL,
`name` text NOT NULL,
`acs_url` text NOT NULL,
`acs_binding` text DEFAULT 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' NOT NULL,
`slo_url` text,
`slo_binding` text,
`cert` text,
`name_id_format` text DEFAULT 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' NOT NULL,
`sign_assertion` integer DEFAULT true NOT NULL,
`sign_response` integer DEFAULT false NOT NULL,
`encrypt_assertion` integer DEFAULT false NOT NULL,
`want_authn_requests_signed` integer DEFAULT false NOT NULL,
`attribute_mapping_json` text,
`enabled` integer DEFAULT true NOT NULL,
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `saml_sps_tenant_entity_id_uidx` ON `saml_sps` (`tenant_id`,`entity_id`);--> statement-breakpoint
CREATE INDEX `saml_sps_tenant_idx` ON `saml_sps` (`tenant_id`);--> statement-breakpoint
CREATE TABLE `sessions` (
`id` text PRIMARY KEY NOT NULL,
`tenant_id` text NOT NULL,
`user_id` text NOT NULL,
`idp_session_id` text NOT NULL,
`amr` text,
`acr` text,
`ip` text,
`user_agent` text,
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
`last_seen_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
`expires_at` integer NOT NULL,
`revoked_at` integer,
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `sessions_idp_session_id_uidx` ON `sessions` (`idp_session_id`);--> statement-breakpoint
CREATE INDEX `sessions_user_idx` ON `sessions` (`user_id`);--> statement-breakpoint
CREATE INDEX `sessions_expires_idx` ON `sessions` (`expires_at`);--> statement-breakpoint
CREATE TABLE `signing_keys` (
`id` text PRIMARY KEY NOT NULL,
`tenant_id` text NOT NULL,
`kid` text NOT NULL,
`use` text DEFAULT 'sig' NOT NULL,
`alg` text NOT NULL,
`public_jwk` text NOT NULL,
`private_jwk_encrypted` text NOT NULL,
`cert_pem` text,
`active` integer DEFAULT true NOT NULL,
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
`rotated_at` integer,
`not_after` integer,
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `signing_keys_tenant_kid_uidx` ON `signing_keys` (`tenant_id`,`kid`);--> statement-breakpoint
CREATE INDEX `signing_keys_tenant_active_idx` ON `signing_keys` (`tenant_id`,`active`);--> statement-breakpoint
CREATE TABLE `tenants` (
`id` text PRIMARY KEY NOT NULL,
`slug` text NOT NULL,
`name` text NOT NULL,
`status` text DEFAULT 'active' NOT NULL,
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `tenants_slug_uidx` ON `tenants` (`slug`);--> statement-breakpoint
CREATE TABLE `users` (
`id` text PRIMARY KEY NOT NULL,
`tenant_id` text NOT NULL,
`email` text NOT NULL,
`email_verified_at` integer,
`display_name` text,
`status` text DEFAULT 'active' NOT NULL,
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
FOREIGN KEY (`tenant_id`) REFERENCES `tenants`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `users_tenant_email_uidx` ON `users` (`tenant_id`,`email`);--> statement-breakpoint
CREATE INDEX `users_tenant_idx` ON `users` (`tenant_id`);
1 change: 1 addition & 0 deletions drizzle/0001_flashy_blackheart.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `users` ADD `role` text DEFAULT 'user' NOT NULL;
2 changes: 2 additions & 0 deletions drizzle/0002_dizzy_korvac.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE `users` ADD `username` text;--> statement-breakpoint
CREATE UNIQUE INDEX `users_tenant_username_uidx` ON `users` (`tenant_id`,`username`);
5 changes: 5 additions & 0 deletions drizzle/0003_demonic_skin.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE TABLE `rate_limits` (
`key` text PRIMARY KEY NOT NULL,
`count` integer DEFAULT 1 NOT NULL,
`expires_at` integer NOT NULL
);
Loading
Loading