From f758b22c56a1ffd461a7ca29e39a899a59b74ada Mon Sep 17 00:00:00 2001 From: Addison LeClair Date: Wed, 14 Oct 2026 08:40:27 +0000 Subject: [PATCH 01/10] feat: add idempotency key handling --- .../drizzle/0033_idempotency_keys.sql | 12 + .../dashboard/drizzle/meta/0033_snapshot.json | 3795 +++++++++++++++++ apps/dashboard/drizzle/meta/_journal.json | 7 + apps/dashboard/src/lib/idempotency.ts | 29 + apps/dashboard/src/lib/server/db/schema.ts | 22 + apps/dashboard/src/lib/server/idempotency.ts | 81 + 6 files changed, 3946 insertions(+) create mode 100644 apps/dashboard/drizzle/0033_idempotency_keys.sql create mode 100644 apps/dashboard/drizzle/meta/0033_snapshot.json create mode 100644 apps/dashboard/src/lib/idempotency.ts create mode 100644 apps/dashboard/src/lib/server/idempotency.ts diff --git a/apps/dashboard/drizzle/0033_idempotency_keys.sql b/apps/dashboard/drizzle/0033_idempotency_keys.sql new file mode 100644 index 00000000..c1200149 --- /dev/null +++ b/apps/dashboard/drizzle/0033_idempotency_keys.sql @@ -0,0 +1,12 @@ +CREATE TYPE "public"."idempotency_status" AS ENUM('pending', 'completed');--> statement-breakpoint +CREATE TABLE "idempotency_keys" ( + "key" text PRIMARY KEY NOT NULL, + "fingerprint" text NOT NULL, + "status" "idempotency_status" DEFAULT 'pending' NOT NULL, + "result" text, + "created_at" bigint DEFAULT (extract(epoch from now()) * 1000)::bigint NOT NULL, + "updated_at" bigint DEFAULT (extract(epoch from now()) * 1000)::bigint NOT NULL, + "expires_at" bigint NOT NULL +); +--> statement-breakpoint +CREATE INDEX "idempotency_keys_expires_at_index" ON "idempotency_keys" USING btree ("expires_at"); \ No newline at end of file diff --git a/apps/dashboard/drizzle/meta/0033_snapshot.json b/apps/dashboard/drizzle/meta/0033_snapshot.json new file mode 100644 index 00000000..626c3dc9 --- /dev/null +++ b/apps/dashboard/drizzle/meta/0033_snapshot.json @@ -0,0 +1,3795 @@ +{ + "id": "c7b6a47c-f933-4600-974a-3f7a8fde0c1e", + "prevId": "697026db-dad0-4b99-9832-98ae4c180edc", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.api_tokens": { + "name": "api_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "api_tokens_user_id_index": { + "name": "api_tokens_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.base_images": { + "name": "base_images", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'bg-gray-600'" + }, + "is_official": { + "name": "is_official", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "logo_svg": { + "name": "logo_svg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#6b7280'" + }, + "image_type": { + "name": "image_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'qcow2'" + }, + "secure_boot": { + "name": "secure_boot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "isa": { + "name": "isa", + "type": "vm_isa", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_meters": { + "name": "billing_meters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "billing_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "units": { + "name": "units", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_metered_at": { + "name": "last_metered_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cap_period_start": { + "name": "cap_period_start", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cap_period_end": { + "name": "cap_period_end", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "hours_this_period": { + "name": "hours_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "billing_meters_resource_index": { + "name": "billing_meters_resource_index", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_meters_active_last_metered_at_index": { + "name": "billing_meters_active_last_metered_at_index", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_metered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_meters_project_active_index": { + "name": "billing_meters_project_active_index", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_usage_events": { + "name": "billing_usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "billing_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "billing_sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "billing_usage_events_idempotency_key_index": { + "name": "billing_usage_events_idempotency_key_index", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_project_created_at_index": { + "name": "billing_usage_events_project_created_at_index", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_sync_status_created_at_index": { + "name": "billing_usage_events_sync_status_created_at_index", + "columns": [ + { + "expression": "sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_usage_events_resource_index": { + "name": "billing_usage_events_resource_index", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_keys": { + "name": "idempotency_keys", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "idempotency_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idempotency_keys_expires_at_index": { + "name": "idempotency_keys_expires_at_index", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ip_assignments": { + "name": "ip_assignments", + "schema": "", + "columns": { + "ip": { + "name": "ip", + "type": "inet", + "primaryKey": false, + "notNull": true + }, + "ip_block_id": { + "name": "ip_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ip_assignments_ip_block_id_index": { + "name": "ip_assignments_ip_block_id_index", + "columns": [ + { + "expression": "ip_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ip_assignments_associated_vm_id_index": { + "name": "ip_assignments_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ip_assignments_ip_block_id_ip_blocks_id_fk": { + "name": "ip_assignments_ip_block_id_ip_blocks_id_fk", + "tableFrom": "ip_assignments", + "tableTo": "ip_blocks", + "columnsFrom": [ + "ip_block_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ip_assignments_associated_vm_id_vms_id_fk": { + "name": "ip_assignments_associated_vm_id_vms_id_fk", + "tableFrom": "ip_assignments", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ip_blocks": { + "name": "ip_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ip_block": { + "name": "ip_block", + "type": "cidr", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ipam_allocations": { + "name": "ipam_allocations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ipam_prefix_id": { + "name": "ipam_prefix_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "family": { + "name": "family", + "type": "ip_family", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "cidr", + "primaryKey": false, + "notNull": false + }, + "prefix_length": { + "name": "prefix_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mac_address": { + "name": "mac_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "ipam_allocations_ipam_prefix_id_index": { + "name": "ipam_allocations_ipam_prefix_id_index", + "columns": [ + { + "expression": "ipam_prefix_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_associated_vm_id_index": { + "name": "ipam_allocations_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_vm_family_index": { + "name": "ipam_allocations_vm_family_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "family", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_address_index": { + "name": "ipam_allocations_address_index", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_allocations_prefix_index": { + "name": "ipam_allocations_prefix_index", + "columns": [ + { + "expression": "prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ipam_allocations_ipam_prefix_id_ipam_prefixes_id_fk": { + "name": "ipam_allocations_ipam_prefix_id_ipam_prefixes_id_fk", + "tableFrom": "ipam_allocations", + "tableTo": "ipam_prefixes", + "columnsFrom": [ + "ipam_prefix_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ipam_allocations_associated_vm_id_vms_id_fk": { + "name": "ipam_allocations_associated_vm_id_vms_id_fk", + "tableFrom": "ipam_allocations", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ipam_prefixes": { + "name": "ipam_prefixes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cidr": { + "name": "cidr", + "type": "cidr", + "primaryKey": false, + "notNull": true + }, + "family": { + "name": "family", + "type": "ip_family", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ipv6_use_transit_address": { + "name": "ipv6_use_transit_address", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "whitelist_start": { + "name": "whitelist_start", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "whitelist_end": { + "name": "whitelist_end", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "gateway_address": { + "name": "gateway_address", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "bunny_dns_zone": { + "name": "bunny_dns_zone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "ipam_prefixes_cidr_index": { + "name": "ipam_prefixes_cidr_index", + "columns": [ + { + "expression": "cidr", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_prefixes_family_disabled_index": { + "name": "ipam_prefixes_family_disabled_index", + "columns": [ + { + "expression": "family", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "disabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ipam_ptr_records": { + "name": "ipam_ptr_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ipam_allocation_id": { + "name": "ipam_allocation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "inet", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bunny_record_id": { + "name": "bunny_record_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "ipam_ptr_records_ipam_allocation_id_index": { + "name": "ipam_ptr_records_ipam_allocation_id_index", + "columns": [ + { + "expression": "ipam_allocation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ipam_ptr_records_address_index": { + "name": "ipam_ptr_records_address_index", + "columns": [ + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ipam_ptr_records_ipam_allocation_id_ipam_allocations_id_fk": { + "name": "ipam_ptr_records_ipam_allocation_id_ipam_allocations_id_fk", + "tableFrom": "ipam_ptr_records", + "tableTo": "ipam_allocations", + "columnsFrom": [ + "ipam_allocation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ipam_settings": { + "name": "ipam_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "default_ptr_format_ipv4": { + "name": "default_ptr_format_ipv4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_ptr_format_ipv6": { + "name": "default_ptr_format_ipv6", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_periods": { + "name": "payment_periods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "vm_id": { + "name": "vm_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate": { + "name": "rate", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "cap": { + "name": "cap", + "type": "numeric", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "payment_periods_vm_id_index": { + "name": "payment_periods_vm_id_index", + "columns": [ + { + "expression": "vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_periods_user_id_index": { + "name": "payment_periods_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_periods_vm_id_vms_id_fk": { + "name": "payment_periods_vm_id_vms_id_fk", + "tableFrom": "payment_periods", + "tableTo": "vms", + "columnsFrom": [ + "vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_billing_customers": { + "name": "project_billing_customers", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "autumn_customer_id": { + "name": "autumn_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_status": { + "name": "sync_status", + "type": "billing_sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "past_due_since": { + "name": "past_due_since", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "project_billing_customers_autumn_customer_id_index": { + "name": "project_billing_customers_autumn_customer_id_index", + "columns": [ + { + "expression": "autumn_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh_keys": { + "name": "ssh_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ssh_keys_user_id_index": { + "name": "ssh_keys_user_id_index", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vm_types": { + "name": "vm_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isa": { + "name": "isa", + "type": "vm_isa", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cores": { + "name": "cores", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ram_capacity": { + "name": "ram_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "storage_amount": { + "name": "storage_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "cap": { + "name": "cap", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "autumn_feature_id": { + "name": "autumn_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vms": { + "name": "vms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proxmox_id": { + "name": "proxmox_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "proxmox_node": { + "name": "proxmox_node", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_known_ipv4": { + "name": "last_known_ipv4", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "last_known_ipv6": { + "name": "last_known_ipv6", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "last_known_status": { + "name": "last_known_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_known_uptime": { + "name": "last_known_uptime", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_known_at": { + "name": "last_known_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "owner_project_id": { + "name": "owner_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vm_type_id": { + "name": "vm_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + }, + "backend": { + "name": "backend", + "type": "vm_backend", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "vm_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'provisioning'" + }, + "status_error": { + "name": "status_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "vms_owner_project_id_index": { + "name": "vms_owner_project_id_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vms_owner_project_active_index": { + "name": "vms_owner_project_active_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vms_proxmox_id_index": { + "name": "vms_proxmox_id_index", + "columns": [ + { + "expression": "proxmox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vms_active_proxmox_id_unique": { + "name": "vms_active_proxmox_id_unique", + "columns": [ + { + "expression": "proxmox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"vms\".\"active\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vms_owner_project_id_organization_id_fk": { + "name": "vms_owner_project_id_organization_id_fk", + "tableFrom": "vms", + "tableTo": "organization", + "columnsFrom": [ + "owner_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vms_vm_type_id_vm_types_id_fk": { + "name": "vms_vm_type_id_vm_types_id_fk", + "tableFrom": "vms", + "tableTo": "vm_types", + "columnsFrom": [ + "vm_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volumes": { + "name": "volumes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_project_id": { + "name": "owner_project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "indexes": { + "volumes_owner_project_id_index": { + "name": "volumes_owner_project_id_index", + "columns": [ + { + "expression": "owner_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "volumes_associated_vm_id_index": { + "name": "volumes_associated_vm_id_index", + "columns": [ + { + "expression": "associated_vm_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "volumes_owner_project_id_organization_id_fk": { + "name": "volumes_owner_project_id_organization_id_fk", + "tableFrom": "volumes", + "tableTo": "organization", + "columnsFrom": [ + "owner_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "volumes_associated_vm_id_vms_id_fk": { + "name": "volumes_associated_vm_id_vms_id_fk", + "tableFrom": "volumes", + "tableTo": "vms", + "columnsFrom": [ + "associated_vm_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_exempt": { + "name": "billing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_exempt": { + "name": "billing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.billing_resource_type": { + "name": "billing_resource_type", + "schema": "public", + "values": [ + "vm", + "volume" + ] + }, + "public.billing_sync_status": { + "name": "billing_sync_status", + "schema": "public", + "values": [ + "pending", + "synced", + "failed", + "abandoned" + ] + }, + "public.idempotency_status": { + "name": "idempotency_status", + "schema": "public", + "values": [ + "pending", + "completed" + ] + }, + "public.ip_family": { + "name": "ip_family", + "schema": "public", + "values": [ + "ipv4", + "ipv6" + ] + }, + "public.vm_backend": { + "name": "vm_backend", + "schema": "public", + "values": [ + "proxmox" + ] + }, + "public.vm_isa": { + "name": "vm_isa", + "schema": "public", + "values": [ + "x86", + "arm", + "risc-v" + ] + }, + "public.vm_status": { + "name": "vm_status", + "schema": "public", + "values": [ + "provisioning", + "ready", + "error", + "deleting" + ] + } + }, + "schemas": { + "analytics": "analytics" + }, + "sequences": { + "public.proxmox_vmid_seq": { + "name": "proxmox_vmid_seq", + "schema": "public", + "increment": "1", + "startWith": "1000", + "minValue": "1000", + "maxValue": "999999999", + "cache": "1", + "cycle": false + } + }, + "roles": {}, + "policies": {}, + "views": { + "analytics.api_tokens": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "definition": "select \"id\", \"user_id\", \"created_at\" from \"api_tokens\"", + "name": "api_tokens", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.base_images": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'bg-gray-600'" + }, + "is_official": { + "name": "is_official", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "logo_svg": { + "name": "logo_svg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#6b7280'" + }, + "image_type": { + "name": "image_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'qcow2'" + }, + "secure_boot": { + "name": "secure_boot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "isa": { + "name": "isa", + "type": "vm_isa", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "definition": "select \"id\", \"file_path\", \"name\", \"version\", \"description\", \"short_name\", \"icon\", \"color\", \"is_official\", \"logo_svg\", \"accent_color\", \"image_type\", \"secure_boot\", \"isa\", \"sort_order\" from \"base_images\"", + "name": "base_images", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.billing_meters": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "billing_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "units": { + "name": "units", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_metered_at": { + "name": "last_metered_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"id\", \"project_id\", \"resource_type\", \"resource_id\", \"feature_id\", \"units\", \"last_metered_at\", \"active\", \"created_at\", \"ended_at\" from \"billing_meters\"", + "name": "billing_meters", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.billing_usage_events": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "billing_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_status": { + "name": "sync_status", + "type": "billing_sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "definition": "select \"id\", \"project_id\", \"resource_type\", \"resource_id\", \"feature_id\", \"quantity\", \"period_start\", \"period_end\", \"idempotency_key\", \"sync_status\", \"sync_error\", \"synced_at\", \"created_at\" from \"billing_usage_events\"", + "name": "billing_usage_events", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.ipam_allocations": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ipam_prefix_id": { + "name": "ipam_prefix_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "family": { + "name": "family", + "type": "ip_family", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "cidr", + "primaryKey": false, + "notNull": false + }, + "prefix_length": { + "name": "prefix_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "definition": "select \"id\", \"ipam_prefix_id\", \"associated_vm_id\", \"family\", \"address\", \"prefix\", \"prefix_length\", \"created_at\" from \"ipam_allocations\"", + "name": "ipam_allocations", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.ipam_prefixes": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cidr": { + "name": "cidr", + "type": "cidr", + "primaryKey": false, + "notNull": true + }, + "family": { + "name": "family", + "type": "ip_family", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ipv6_use_transit_address": { + "name": "ipv6_use_transit_address", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "whitelist_start": { + "name": "whitelist_start", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "whitelist_end": { + "name": "whitelist_end", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "gateway_address": { + "name": "gateway_address", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "definition": "select \"id\", \"name\", \"cidr\", \"family\", \"disabled\", \"ipv6_use_transit_address\", \"whitelist_start\", \"whitelist_end\", \"gateway_address\", \"created_at\" from \"ipam_prefixes\"", + "name": "ipam_prefixes", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.ipam_ptr_records": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ipam_allocation_id": { + "name": "ipam_allocation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "inet", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "definition": "select \"id\", \"ipam_allocation_id\", \"address\", \"value\", \"created_at\" from \"ipam_ptr_records\"", + "name": "ipam_ptr_records", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.ipam_settings": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "default_ptr_format_ipv4": { + "name": "default_ptr_format_ipv4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_ptr_format_ipv6": { + "name": "default_ptr_format_ipv6", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"id\", \"default_ptr_format_ipv4\", \"default_ptr_format_ipv6\" from \"ipam_settings\"", + "name": "ipam_settings", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.member": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "definition": "select \"id\", \"organization_id\", \"user_id\", \"role\", \"created_at\" from \"member\"", + "name": "member", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.organization": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "billing_exempt": { + "name": "billing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"id\", \"name\", \"slug\", \"created_at\", \"billing_exempt\", \"disabled\", \"deleted_at\" from \"organization\"", + "name": "organization", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.project_billing_customers": { + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sync_status": { + "name": "sync_status", + "type": "billing_sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "past_due_since": { + "name": "past_due_since", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "definition": "select \"project_id\", \"sync_status\", \"sync_error\", \"last_synced_at\", \"past_due_since\", \"suspended_at\", \"created_at\", \"updated_at\" from \"project_billing_customers\"", + "name": "project_billing_customers", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.ssh_keys": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "definition": "select \"id\", \"user_id\", \"fingerprint\" from \"ssh_keys\"", + "name": "ssh_keys", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.user": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_exempt": { + "name": "billing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "definition": "select \"id\", \"email_verified\", \"created_at\", \"updated_at\", \"role\", \"banned\", \"ban_expires\", \"two_factor_enabled\", \"is_admin\", \"billing_exempt\" from \"user\"", + "name": "user", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.vm_types": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isa": { + "name": "isa", + "type": "vm_isa", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cores": { + "name": "cores", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ram_capacity": { + "name": "ram_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "storage_amount": { + "name": "storage_amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rate": { + "name": "rate", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "cap": { + "name": "cap", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "definition": "select \"id\", \"name\", \"isa\", \"cores\", \"ram_capacity\", \"storage_amount\", \"rate\", \"cap\", \"sort_order\" from \"vm_types\"", + "name": "vm_types", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.vms": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "proxmox_id": { + "name": "proxmox_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "proxmox_node": { + "name": "proxmox_node", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_known_ipv4": { + "name": "last_known_ipv4", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "last_known_ipv6": { + "name": "last_known_ipv6", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "last_known_status": { + "name": "last_known_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_known_uptime": { + "name": "last_known_uptime", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_known_at": { + "name": "last_known_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "owner_project_id": { + "name": "owner_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vm_type_id": { + "name": "vm_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + }, + "backend": { + "name": "backend", + "type": "vm_backend", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "vm_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'provisioning'" + }, + "status_error": { + "name": "status_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "select \"id\", \"proxmox_id\", \"proxmox_node\", \"last_known_ipv4\", \"last_known_ipv6\", \"last_known_status\", \"last_known_uptime\", \"last_known_at\", \"active\", \"deleted_at\", \"owner_project_id\", \"vm_type_id\", \"creation_date\", \"created_at\", \"backend\", \"status\", \"status_error\" from \"vms\"", + "name": "vms", + "schema": "analytics", + "isExisting": false, + "materialized": false + }, + "analytics.volumes": { + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner_project_id": { + "name": "owner_project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "associated_vm_id": { + "name": "associated_vm_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "(extract(epoch from now()) * 1000)::bigint" + } + }, + "definition": "select \"id\", \"size\", \"owner_project_id\", \"associated_vm_id\", \"created_at\" from \"volumes\"", + "name": "volumes", + "schema": "analytics", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dashboard/drizzle/meta/_journal.json b/apps/dashboard/drizzle/meta/_journal.json index 8abf54af..f9586342 100644 --- a/apps/dashboard/drizzle/meta/_journal.json +++ b/apps/dashboard/drizzle/meta/_journal.json @@ -232,6 +232,13 @@ "when": 1789916661919, "tag": "0032_unique_active_proxmox_id", "breakpoints": true + }, + { + "idx": 33, + "version": "7", + "when": 1791959839230, + "tag": "0033_idempotency_keys", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/dashboard/src/lib/idempotency.ts b/apps/dashboard/src/lib/idempotency.ts new file mode 100644 index 00000000..01487862 --- /dev/null +++ b/apps/dashboard/src/lib/idempotency.ts @@ -0,0 +1,29 @@ +import { ulid } from 'ulid'; + +export type RequestDetails = Record< + string, + string | number | boolean | null | undefined | readonly string[] +>; + +export async function requestFingerprint(operation: string, details: RequestDetails) { + const entries = Object.entries(details) + .filter(([, value]) => value !== undefined) + .sort(([a], [b]) => a.localeCompare(b)); + const bytes = new TextEncoder().encode(JSON.stringify([operation, entries])); + const hash = await crypto.subtle.digest('SHA-256', bytes); + return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, '0')).join(''); +} + +export async function prepareIdempotentRequest(operation: string, details: RequestDetails) { + const storageKey = `request:${await requestFingerprint(operation, details)}`; + const key = sessionStorage.getItem(storageKey) ?? ulid(); + sessionStorage.setItem(storageKey, key); + return { + key, + complete() { + if (sessionStorage.getItem(storageKey) === key) { + sessionStorage.removeItem(storageKey); + } + } + }; +} diff --git a/apps/dashboard/src/lib/server/db/schema.ts b/apps/dashboard/src/lib/server/db/schema.ts index 2fbb8a96..42e3503c 100644 --- a/apps/dashboard/src/lib/server/db/schema.ts +++ b/apps/dashboard/src/lib/server/db/schema.ts @@ -399,6 +399,28 @@ export const sshKeys = pgTable( (table) => [index('ssh_keys_user_id_index').on(table.userId)] ); +// Idempotency Keys + +export const idempotencyStatusEnum = pgEnum('idempotency_status', ['pending', 'completed']); + +export const idempotencyKeys = pgTable( + 'idempotency_keys', + { + key: text('key').primaryKey(), + fingerprint: text('fingerprint').notNull(), + status: idempotencyStatusEnum('status').notNull().default('pending'), + result: text('result'), + createdAt: bigint('created_at', { mode: 'number' }) + .notNull() + .default(sql`(extract(epoch from now()) * 1000)::bigint`), + updatedAt: bigint('updated_at', { mode: 'number' }) + .notNull() + .default(sql`(extract(epoch from now()) * 1000)::bigint`), + expiresAt: bigint('expires_at', { mode: 'number' }).notNull() + }, + (table) => [index('idempotency_keys_expires_at_index').on(table.expiresAt)] +); + // API Tokens export const apiTokens = pgTable( diff --git a/apps/dashboard/src/lib/server/idempotency.ts b/apps/dashboard/src/lib/server/idempotency.ts new file mode 100644 index 00000000..72ea6758 --- /dev/null +++ b/apps/dashboard/src/lib/server/idempotency.ts @@ -0,0 +1,81 @@ +import { error } from '@sveltejs/kit'; +import { and, eq, inArray, lt, or } from 'drizzle-orm'; +import { type RequestDetails, requestFingerprint } from '#lib/idempotency.js'; +import { type Database, initDrizzle } from '#lib/server/db/index.js'; +import { idempotencyKeys } from '#lib/server/db/schema.js'; + +const COMPLETED_TTL_MS = 24 * 60 * 60 * 1000; +const ABANDONED_PENDING_MS = 60 * 60 * 1000; +type JsonResult = string | number | boolean | null | { [key: string]: JsonResult | undefined }; + +export async function withIdempotency( + db: Pick, + options: { key: string; operation: string; args: RequestDetails }, + fn: () => Promise +): Promise { + const { key, operation, args } = options; + const fingerprint = await requestFingerprint(operation, args); + const [claimed] = await db + .insert(idempotencyKeys) + .values({ key, fingerprint, expiresAt: Date.now() + COMPLETED_TTL_MS }) + .onConflictDoNothing({ target: idempotencyKeys.key }) + .returning({ key: idempotencyKeys.key }); + + if (!claimed) { + const [existing] = await db + .select() + .from(idempotencyKeys) + .where(eq(idempotencyKeys.key, key)) + .limit(1); + if (existing && existing.fingerprint !== fingerprint) { + error(422, 'This request was already submitted with different details.'); + } + if (existing?.status === 'completed' && existing.result !== null) { + return JSON.parse(existing.result) as T; + } + error( + 409, + 'This request has already started. Check its status before submitting another. If it remains unresolved, contact support.' + ); + } + + let result: T; + try { + result = await fn(); + } catch (err) { + await db.delete(idempotencyKeys).where(eq(idempotencyKeys.key, key)); + throw err; + } + const now = Date.now(); + await db + .update(idempotencyKeys) + .set({ + status: 'completed', + result: JSON.stringify(result), + updatedAt: now, + expiresAt: now + COMPLETED_TTL_MS + }) + .where(eq(idempotencyKeys.key, key)); + return result; +} + +export async function purgeExpiredIdempotencyKeys(now = Date.now(), limit = 500) { + const db = initDrizzle(); + const expired = or( + and(eq(idempotencyKeys.status, 'completed'), lt(idempotencyKeys.expiresAt, now)), + and( + eq(idempotencyKeys.status, 'pending'), + lt(idempotencyKeys.createdAt, now - ABANDONED_PENDING_MS) + ) + ); + const batch = db + .select({ key: idempotencyKeys.key }) + .from(idempotencyKeys) + .where(expired) + .limit(limit); + const deleted = await db + .delete(idempotencyKeys) + .where(and(expired, inArray(idempotencyKeys.key, batch))) + .returning({ key: idempotencyKeys.key }); + return { purged: deleted.length }; +} From 605e92b1af16b02ec6d4d0f115da4b4f70987f97 Mon Sep 17 00:00:00 2001 From: Addison LeClair Date: Wed, 14 Oct 2026 08:41:09 +0000 Subject: [PATCH 02/10] feat: make credit purchases idempotent --- .../lib/components/buy-credits-dialog.svelte | 41 +++++++++++++++---- .../src/lib/remote/billing.remote.ts | 33 ++++++++++++--- .../src/lib/server/billing/autumn.ts | 17 ++++---- .../api/internal/billing/meter/+server.ts | 10 ++++- 4 files changed, 79 insertions(+), 22 deletions(-) diff --git a/apps/dashboard/src/lib/components/buy-credits-dialog.svelte b/apps/dashboard/src/lib/components/buy-credits-dialog.svelte index 75fd8657..0bcfcdf9 100644 --- a/apps/dashboard/src/lib/components/buy-credits-dialog.svelte +++ b/apps/dashboard/src/lib/components/buy-credits-dialog.svelte @@ -3,8 +3,10 @@ import * as Dialog from '#lib/components/ui/dialog/index.js'; import { Input } from '#lib/components/ui/input/index.js'; import { Label } from '#lib/components/ui/label/index.js'; + import { prepareIdempotentRequest } from '#lib/idempotency.js'; import { purchaseCredits } from '#lib/remote/billing.remote.js'; import { getErrorMessage } from '#lib/utils.js'; + import { page } from '$app/state'; import Loader2 from '~icons/lucide/loader-2'; import DollarSign from '~icons/nucleo/dollar-sign'; @@ -33,6 +35,7 @@ let loading = $state(false); let actionError = $state(''); let amountInput = $state('10'); + let purchaseResult = $state<{ url: string | null; complete: () => void } | null>(null); const amount = $derived(Number.parseFloat(amountInput)); const amountValid = $derived(Number.isFinite(amount) && amount > 0); @@ -55,21 +58,36 @@ } async function handlePurchase() { - if (!projectId || loading || !canSubmit) { + if (!projectId || loading || !(purchaseResult || canSubmit)) { return; } loading = true; actionError = ''; try { - const result = await purchaseCredits({ projectId, credits }); - if (result.url) { - window.location.href = result.url; + if (!purchaseResult) { + const request = await prepareIdempotentRequest('billing.purchaseCredits', { + projectId, + credits, + userId: page.data.user?.id + }); + const result = await purchaseCredits({ projectId, credits, idempotencyKey: request.key }); + purchaseResult = { ...result, complete: request.complete }; + } + if (purchaseResult.url) { + window.location.href = purchaseResult.url; + purchaseResult.complete(); return; } window.location.reload(); + purchaseResult.complete(); } catch (err) { - actionError = getErrorMessage(err, 'Credits could not be purchased. Try again.'); + actionError = purchaseResult + ? 'Your purchase was submitted. Select Continue to open the payment page or refresh your balance.' + : getErrorMessage( + err, + 'The purchase could not be confirmed. Check your billing history before trying again.' + ); loading = false; } } @@ -168,6 +186,7 @@ {#if actionError} From b04964cc74b1bb5564d2649941ed1797f4927f21 Mon Sep 17 00:00:00 2001 From: Addison LeClair Date: Wed, 14 Oct 2026 08:49:47 +0000 Subject: [PATCH 04/10] feat make admin vm creation idempotent --- .../src/lib/remote/admin-projects.remote.ts | 36 ++++++++++++------- .../(app)/admin/projects/[[id]]/+page.svelte | 11 ++++-- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/apps/dashboard/src/lib/remote/admin-projects.remote.ts b/apps/dashboard/src/lib/remote/admin-projects.remote.ts index 351481f7..47779745 100644 --- a/apps/dashboard/src/lib/remote/admin-projects.remote.ts +++ b/apps/dashboard/src/lib/remote/admin-projects.remote.ts @@ -23,6 +23,7 @@ import { member, organization, user } from '#lib/server/db/auth.schema.js'; import { initDrizzle } from '#lib/server/db/index.js'; import { projectBillingCustomers, sshKeys, vms, volumes } from '#lib/server/db/schema.js'; import { sendRenderedEmail } from '#lib/server/email.js'; +import { withIdempotency } from '#lib/server/idempotency.js'; import { captureServerEvent } from '#lib/server/posthog.js'; import { softDeleteOrganizationResources } from '#lib/server/project-deletion.js'; import { provisionVm } from '#lib/server/vm-provisioning.js'; @@ -233,7 +234,8 @@ const createVmParams = type({ networkingMode: "'both' | 'ipv6'?", imageId: 'string?', sshKeyIds: 'string[]?', - password: 'string?' + password: 'string?', + idempotencyKey: '0 < string <= 128' }); export const createAdminVm = command(createVmParams, async (params) => { const db = await requireCurrentAdmin(); @@ -270,17 +272,27 @@ export const createAdminVm = command(createVmParams, async (params) => { : []; const publicKeys = keys.filter((key) => sshKeyIds.includes(key.id)).map((key) => key.publicKey); - return provisionVm(db, { - projectId: params.projectId, - vmTypeId: params.vmTypeId, - name: params.name, - userId: owner?.userId ?? adminUser.id, - billingExempt, - networkingMode: params.networkingMode, - imageId: params.imageId, - sshPublicKeys: publicKeys, - password: params.password - }); + const { idempotencyKey, ...operationArgs } = params; + return withIdempotency( + db, + { + key: idempotencyKey, + operation: 'admin.createVm', + args: { ...operationArgs, userId: adminUser.id, sshKeyIds: [...sshKeyIds].sort() } + }, + () => + provisionVm(db, { + projectId: params.projectId, + vmTypeId: params.vmTypeId, + name: params.name, + userId: owner?.userId ?? adminUser.id, + billingExempt, + networkingMode: params.networkingMode, + imageId: params.imageId, + sshPublicKeys: publicKeys, + password: params.password + }) + ); }); const setDisabledParams = type({ projectId: 'string', disabled: 'boolean' }); diff --git a/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte b/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte index 332d41b3..170f06b2 100644 --- a/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte +++ b/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte @@ -12,6 +12,7 @@ import * as Sheet from '#lib/components/ui/sheet/index.js'; import { Switch } from '#lib/components/ui/switch/index.js'; import { confirmDestructive } from '#lib/confirm.svelte.js'; + import { prepareIdempotentRequest } from '#lib/idempotency.js'; import { generateServerName } from '#lib/name-generator.js'; import { type AdminProject, @@ -322,16 +323,22 @@ createVmSaving = true; createVmError = ''; try { - await createAdminVm({ + const payload = { projectId: createVmProject.id, vmTypeId: createVmTypeId, name: createVmName.trim(), networkingMode: createVmNetworking, ...(createVmImageId ? { imageId: createVmImageId } : {}), - ...(createVmSshKeyIds.length > 0 ? { sshKeyIds: createVmSshKeyIds } : {}), + ...(createVmSshKeyIds.length > 0 ? { sshKeyIds: [...createVmSshKeyIds].sort() } : {}), ...(createVmPassword.trim() ? { password: createVmPassword.trim() } : {}) + }; + const request = await prepareIdempotentRequest('admin.createVm', { + ...payload, + userId: page.data.user?.id }); + await createAdminVm({ ...payload, idempotencyKey: request.key }); await Promise.all([invalidate('app:admin-vms'), invalidate('app:admin-projects')]); + request.complete(); createVmOpen = false; toast.success('Server created'); } catch (err) { From d43e03e6a168fda5115cc0324f04ce93991298aa Mon Sep 17 00:00:00 2001 From: Addison LeClair Date: Wed, 14 Oct 2026 08:52:49 +0000 Subject: [PATCH 05/10] feat: make campaign email sends idempotent --- .../src/lib/remote/email-campaign.remote.ts | 71 ++++++++++++------- apps/dashboard/src/lib/server/idempotency.ts | 8 ++- .../routes/(app)/admin/emails/+page.svelte | 20 +++++- 3 files changed, 70 insertions(+), 29 deletions(-) diff --git a/apps/dashboard/src/lib/remote/email-campaign.remote.ts b/apps/dashboard/src/lib/remote/email-campaign.remote.ts index c9c547b2..43fc6ed2 100644 --- a/apps/dashboard/src/lib/remote/email-campaign.remote.ts +++ b/apps/dashboard/src/lib/remote/email-campaign.remote.ts @@ -20,6 +20,7 @@ import VerifyEmailEmail from '#lib/emails/verify-email.svelte'; import { requireAdmin } from '#lib/server/auth-context.js'; import { initDrizzle } from '#lib/server/db/index.js'; import { emailToPlainText, renderEmail, sendEmail } from '#lib/server/email.js'; +import { withIdempotency } from '#lib/server/idempotency.js'; import { command, getRequestEvent } from '$app/server'; const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -144,11 +145,12 @@ const sendParams = type({ subject: 'string', fields: csvRow, rows: csvRow.array(), - emailColumn: 'string' + emailColumn: 'string', + idempotencyKey: '0 < string <= 128' }); export const sendCampaignEmails = command(sendParams, async (params) => { - await requireCurrentAdmin(); + const { db, userId } = await requireCurrentAdmin(); const { meta, component } = resolveTemplate(params.template); if (params.rows.length === 0) { error(400, 'No recipients provided'); @@ -157,31 +159,48 @@ export const sendCampaignEmails = command(sendParams, async (params) => { error(400, `Send at most ${CAMPAIGN_BATCH_SIZE} recipients per batch`); } - let sent = 0; - const failures: { recipient: string; reason: string }[] = []; - - for (const row of params.rows) { - const to = (row[params.emailColumn] ?? '').trim(); - try { - if (!EMAIL_PATTERN.test(to)) { - throw new Error('Invalid email address'); + return withIdempotency( + db, + { + key: params.idempotencyKey, + operation: 'admin.sendCampaignEmails', + args: { + template: params.template, + subject: params.subject, + fields: JSON.stringify(params.fields), + rows: JSON.stringify(params.rows), + emailColumn: params.emailColumn, + userId } - const subject = applyPlaceholders(params.subject, row).trim(); - if (subject === '') { - throw new Error('Subject is empty for this recipient'); + }, + async () => { + let sent = 0; + const failures: { recipient: string; reason: string }[] = []; + + for (const row of params.rows) { + const to = (row[params.emailColumn] ?? '').trim(); + try { + if (!EMAIL_PATTERN.test(to)) { + throw new Error('Invalid email address'); + } + const subject = applyPlaceholders(params.subject, row).trim(); + if (subject === '') { + throw new Error('Subject is empty for this recipient'); + } + // biome-ignore lint/performance/noAwaitInLoops: campaign sends go out one at a time to stay under the email provider's rate limit + const html = await renderCampaignHtml(meta, component, params.fields, row); + const text = await emailToPlainText(html); + await sendEmail({ subject, to, html, text }); + sent += 1; + } catch (err) { + failures.push({ + recipient: to === '' ? '(missing email)' : to, + reason: err instanceof Error ? err.message : 'Send failed' + }); + } } - // biome-ignore lint/performance/noAwaitInLoops: campaign sends go out one at a time to stay under the email provider's rate limit - const html = await renderCampaignHtml(meta, component, params.fields, row); - const text = await emailToPlainText(html); - await sendEmail({ subject, to, html, text }); - sent += 1; - } catch (err) { - failures.push({ - recipient: to === '' ? '(missing email)' : to, - reason: err instanceof Error ? err.message : 'Send failed' - }); - } - } - return { sent, failures }; + return { sent, failures }; + } + ); }); diff --git a/apps/dashboard/src/lib/server/idempotency.ts b/apps/dashboard/src/lib/server/idempotency.ts index 72ea6758..565c95aa 100644 --- a/apps/dashboard/src/lib/server/idempotency.ts +++ b/apps/dashboard/src/lib/server/idempotency.ts @@ -6,7 +6,13 @@ import { idempotencyKeys } from '#lib/server/db/schema.js'; const COMPLETED_TTL_MS = 24 * 60 * 60 * 1000; const ABANDONED_PENDING_MS = 60 * 60 * 1000; -type JsonResult = string | number | boolean | null | { [key: string]: JsonResult | undefined }; +type JsonResult = + | string + | number + | boolean + | null + | JsonResult[] + | { [key: string]: JsonResult | undefined }; export async function withIdempotency( db: Pick, diff --git a/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte b/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte index 0fb43fac..375593e0 100644 --- a/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte +++ b/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte @@ -11,6 +11,7 @@ extractPlaceholders, fieldToken } from '#lib/emails/campaign-registry.js'; + import { prepareIdempotentRequest } from '#lib/idempotency.js'; import type { AdminUser } from '#lib/remote/admin-users.remote.js'; import { previewCampaignEmail, @@ -19,6 +20,7 @@ } from '#lib/remote/email-campaign.remote.js'; import { type AdminPageData, AdminState } from '#lib/state/admin.svelte.js'; import { getErrorMessage } from '#lib/utils.js'; + import { page } from '$app/state'; import Check from '~icons/lucide/check'; import Loader2 from '~icons/lucide/loader-2'; import Plus from '~icons/lucide/plus'; @@ -390,19 +392,33 @@ sendError = ''; const rows = $state.snapshot(recipientRows); const fields = $state.snapshot(fieldValues); + const sentBatches: (() => void)[] = []; try { for (let i = 0; i < rows.length; i += CAMPAIGN_BATCH_SIZE) { - // biome-ignore lint/performance/noAwaitInLoops: batches go out one at a time to stay under the email provider's rate limit and report progress - const result = await sendCampaignEmails({ + const batch = { template: selectedTemplateKey, subject, fields, rows: rows.slice(i, i + CAMPAIGN_BATCH_SIZE), emailColumn: 'email' + }; + // biome-ignore lint/performance/noAwaitInLoops: batches go out one at a time to stay under the email provider's rate limit and report progress + const request = await prepareIdempotentRequest('admin.sendCampaignEmails', { + template: batch.template, + subject: batch.subject, + fields: JSON.stringify(batch.fields), + rows: JSON.stringify(batch.rows), + emailColumn: batch.emailColumn, + userId: page.data.user?.id }); + const result = await sendCampaignEmails({ ...batch, idempotencyKey: request.key }); + sentBatches.push(request.complete); sentCount += result.sent; sendFailures = [...sendFailures, ...result.failures]; } + for (const complete of sentBatches) { + complete(); + } sendComplete = true; } catch (err) { sendError = getErrorMessage(err, 'Failed to send emails'); From 24812bb74c280d1c8808115bd0d362a97a5da3ae Mon Sep 17 00:00:00 2001 From: Addison LeClair Date: Wed, 14 Oct 2026 08:54:59 +0000 Subject: [PATCH 06/10] feat: make project creation idempotent --- .../src/lib/remote/projects.remote.ts | 42 +++++++++++-------- apps/dashboard/src/routes/(app)/+page.svelte | 9 +++- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/apps/dashboard/src/lib/remote/projects.remote.ts b/apps/dashboard/src/lib/remote/projects.remote.ts index 6a3f1cec..88ce9592 100644 --- a/apps/dashboard/src/lib/remote/projects.remote.ts +++ b/apps/dashboard/src/lib/remote/projects.remote.ts @@ -16,6 +16,7 @@ import { } from '#lib/server/billing/autumn.js'; import { member, organization } from '#lib/server/db/auth.schema.js'; import { initDrizzle } from '#lib/server/db/index.js'; +import { withIdempotency } from '#lib/server/idempotency.js'; import { captureServerEvent } from '#lib/server/posthog.js'; import { softDeleteOrganizationResources } from '#lib/server/project-deletion.js'; import { command, getRequestEvent, query } from '$app/server'; @@ -200,31 +201,38 @@ export const getProject = query(getParams, async (params): Promise => }; }); -const createParams = type({ name: 'string' }); -export const createProject = command(createParams, async (params) => { +const createParams = type({ name: 'string', idempotencyKey: '0 < string <= 128' }); +export const createProject = command(createParams, (params) => { const event = getRequestEvent(); if (!event?.locals.user) { error(401, 'Authentication required'); } + const userId = event.locals.user.id; const name = toProjectName(params.name); - const auth = initAuth(); - const org = await auth.api.createOrganization({ - headers: event.request.headers, - body: { - name, - slug: toSlug(name) - } - }); + return withIdempotency( + initDrizzle(), + { key: params.idempotencyKey, operation: 'projects.createProject', args: { name, userId } }, + async () => { + const auth = initAuth(); + const org = await auth.api.createOrganization({ + headers: event.request.headers, + body: { + name, + slug: toSlug(name) + } + }); - await ensureLocalProjectBillingCustomer(org.id); - ensureProjectCustomer(org.id).catch((err) => { - console.warn(`Failed to sync Autumn customer for project ${org.id}`, err); - }); - clearProjectListCache(event.locals.user.id); - captureServerEvent('project_created', {}, { projectId: org.id }); + await ensureLocalProjectBillingCustomer(org.id); + ensureProjectCustomer(org.id).catch((err) => { + console.warn(`Failed to sync Autumn customer for project ${org.id}`, err); + }); + clearProjectListCache(userId); + captureServerEvent('project_created', {}, { projectId: org.id }); - return { id: org.id }; + return { id: org.id }; + } + ); }); const deleteParams = type({ projectId: 'string' }); diff --git a/apps/dashboard/src/routes/(app)/+page.svelte b/apps/dashboard/src/routes/(app)/+page.svelte index 004555c1..17dba84d 100644 --- a/apps/dashboard/src/routes/(app)/+page.svelte +++ b/apps/dashboard/src/routes/(app)/+page.svelte @@ -7,12 +7,14 @@ import * as DropdownMenu from '#lib/components/ui/dropdown-menu/index.js'; import { Input } from '#lib/components/ui/input/index.js'; import { Label } from '#lib/components/ui/label/index.js'; + import { prepareIdempotentRequest } from '#lib/idempotency.js'; import { createProject as createProjectRpc, deleteProject as deleteProjectRpc } from '#lib/remote/projects.remote.js'; import { getErrorMessage } from '#lib/utils.js'; import { goto, invalidate } from '$app/navigation'; + import { page } from '$app/state'; import ArrowRight from '~icons/lucide/arrow-right'; import Loader2 from '~icons/lucide/loader-2'; import MoreHorizontal from '~icons/lucide/more-horizontal'; @@ -49,7 +51,12 @@ createProjectError = ''; try { const name = newProjectName.trim(); - const res = await createProjectRpc({ name }); + const request = await prepareIdempotentRequest('projects.createProject', { + name, + userId: page.data.user?.id + }); + const res = await createProjectRpc({ name, idempotencyKey: request.key }); + request.complete(); projects = [...projects, { id: res.id, projectName: name, role: 'owner' }]; newProjectName = ''; createOpen = false; From 5469936a709de91db0a7edd1af4186808248e002 Mon Sep 17 00:00:00 2001 From: Addison LeClair Date: Wed, 14 Oct 2026 08:55:53 +0000 Subject: [PATCH 07/10] feat: make admin project creation idempotent --- .../src/lib/remote/admin-projects.remote.ts | 54 ++++++++++++------- .../(app)/admin/projects/[[id]]/+page.svelte | 9 ++-- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/apps/dashboard/src/lib/remote/admin-projects.remote.ts b/apps/dashboard/src/lib/remote/admin-projects.remote.ts index 47779745..a3c4ffc6 100644 --- a/apps/dashboard/src/lib/remote/admin-projects.remote.ts +++ b/apps/dashboard/src/lib/remote/admin-projects.remote.ts @@ -197,9 +197,17 @@ function toSlug(name: string) { return `${slug || 'project'}-${Date.now().toString(36)}`; } -const createProjectParams = type({ name: 'string', ownerUserId: 'string' }); +const createProjectParams = type({ + name: 'string', + ownerUserId: 'string', + idempotencyKey: '0 < string <= 128' +}); export const createAdminProject = command(createProjectParams, async (params) => { const db = await requireCurrentAdmin(); + const adminUser = getRequestEvent().locals.user; + if (!adminUser) { + error(401, 'Authentication required'); + } const owner = await db.query.user.findFirst({ where: eq(user.id, params.ownerUserId) }); if (!owner) { @@ -207,24 +215,34 @@ export const createAdminProject = command(createProjectParams, async (params) => } const name = params.name.trim() || 'Untitled Project'; - const auth = initAuth(); - const org = await auth.api.createOrganization({ - body: { - name, - slug: toSlug(name), - userId: params.ownerUserId + return withIdempotency( + db, + { + key: params.idempotencyKey, + operation: 'admin.createProject', + args: { name, ownerUserId: params.ownerUserId, userId: adminUser.id } + }, + async () => { + const auth = initAuth(); + const org = await auth.api.createOrganization({ + body: { + name, + slug: toSlug(name), + userId: params.ownerUserId + } + }); + if (!org) { + error(502, 'Failed to create project'); + } + + await ensureLocalProjectBillingCustomer(org.id); + ensureProjectCustomer(org.id).catch((err) => { + console.warn(`Failed to sync Autumn customer for project ${org.id}`, err); + }); + + return { id: org.id }; } - }); - if (!org) { - error(502, 'Failed to create project'); - } - - await ensureLocalProjectBillingCustomer(org.id); - ensureProjectCustomer(org.id).catch((err) => { - console.warn(`Failed to sync Autumn customer for project ${org.id}`, err); - }); - - return { id: org.id }; + ); }); const createVmParams = type({ diff --git a/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte b/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte index 170f06b2..bca64447 100644 --- a/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte +++ b/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte @@ -247,11 +247,14 @@ createProjectSaving = true; createProjectError = ''; try { - const created = await createAdminProject({ - name: createProjectName.trim(), - ownerUserId: createProjectOwnerId + const payload = { name: createProjectName.trim(), ownerUserId: createProjectOwnerId }; + const request = await prepareIdempotentRequest('admin.createProject', { + ...payload, + userId: page.data.user?.id }); + const created = await createAdminProject({ ...payload, idempotencyKey: request.key }); await invalidate('app:admin-projects'); + request.complete(); createProjectOpen = false; toast.success('Project created'); goto(resolve(`admin/projects/${created.id}`), { reset: false }); From 8255518dcdbced09a32dad22ea0b1893154ffcea Mon Sep 17 00:00:00 2001 From: Addison LeClair Date: Wed, 14 Oct 2026 08:57:28 +0000 Subject: [PATCH 08/10] feat: make ssh key creation idempotent --- .../dialogs/user-settings-dialog.svelte | 10 ++++- .../src/lib/remote/ssh-keys.remote.ts | 40 ++++++++++++------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte index 15a90b8f..dbe46ba9 100644 --- a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte +++ b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte @@ -12,6 +12,7 @@ import { Skeleton } from '#lib/components/ui/skeleton/index.js'; import * as Tabs from '#lib/components/ui/tabs/index.js'; import { confirmDestructive } from '#lib/confirm.svelte.js'; + import { prepareIdempotentRequest } from '#lib/idempotency.js'; import { createApiToken, listApiTokens, revokeApiToken } from '#lib/remote/api-tokens.remote.js'; import { getPendingEmailChange, requestEmailChange } from '#lib/remote/email-change.remote.js'; import { hasPassword } from '#lib/remote/password-change.remote.js'; @@ -25,6 +26,7 @@ import { getErrorMessage } from '#lib/utils.js'; import { goto, invalidate } from '$app/navigation'; import { resolve } from '$app/paths'; + import { page } from '$app/state'; import Check from '~icons/lucide/check'; import Loader2 from '~icons/lucide/loader-2'; import Minus from '~icons/lucide/minus'; @@ -358,9 +360,15 @@ } sshKeyAdding = true; try { - const res = await createSshKeyRpc({ name: newKeyName.trim(), publicKey: newKeyValue.trim() }); + const payload = { name: newKeyName.trim(), publicKey: newKeyValue.trim() }; + const request = await prepareIdempotentRequest('sshKeys.createSshKey', { + ...payload, + userId: page.data.user?.id + }); + const res = await createSshKeyRpc({ ...payload, idempotencyKey: request.key }); await invalidate('app:ssh-keys'); + request.complete(); sshKeys.push({ id: res.id, diff --git a/apps/dashboard/src/lib/remote/ssh-keys.remote.ts b/apps/dashboard/src/lib/remote/ssh-keys.remote.ts index cbbfacbc..7f332484 100644 --- a/apps/dashboard/src/lib/remote/ssh-keys.remote.ts +++ b/apps/dashboard/src/lib/remote/ssh-keys.remote.ts @@ -3,6 +3,7 @@ import { type } from 'arktype'; import { and, eq } from 'drizzle-orm'; import { initDrizzle } from '#lib/server/db/index.js'; import { sshKeys } from '#lib/server/db/schema.js'; +import { withIdempotency } from '#lib/server/idempotency.js'; import { captureServerEvent } from '#lib/server/posthog.js'; import { command, getRequestEvent, query } from '$app/server'; @@ -25,7 +26,8 @@ export const listSshKeys = query(() => { const createParams = type({ name: 'string', publicKey: 'string', - description: 'string?' + description: 'string?', + idempotencyKey: '0 < string <= 128' }); export const createSshKey = command(createParams, async (params) => { @@ -57,20 +59,28 @@ export const createSshKey = command(createParams, async (params) => { error(400, 'Invalid SSH public key: could not compute fingerprint'); } - const [inserted] = await db - .insert(sshKeys) - .values({ - userId: event.locals.user.id, - name: params.name, - publicKey: params.publicKey.trim(), - fingerprint, - description: params.description ?? null - }) - .returning(); - - captureServerEvent('ssh_key_created', { key_type: parts[0] }); - - return { id: inserted.id, fingerprint: inserted.fingerprint }; + const userId = event.locals.user.id; + const { idempotencyKey, ...operationArgs } = params; + return withIdempotency( + db, + { key: idempotencyKey, operation: 'sshKeys.createSshKey', args: { ...operationArgs, userId } }, + async () => { + const [inserted] = await db + .insert(sshKeys) + .values({ + userId, + name: params.name, + publicKey: params.publicKey.trim(), + fingerprint, + description: params.description ?? null + }) + .returning(); + + captureServerEvent('ssh_key_created', { key_type: parts[0] }); + + return { id: inserted.id, fingerprint: inserted.fingerprint }; + } + ); }); const deleteParams = type({ keyId: 'string' }); From dc334a031d08b6cf8ab98fbb6412672fd4b9df87 Mon Sep 17 00:00:00 2001 From: Addison LeClair Date: Wed, 14 Oct 2026 08:59:58 +0000 Subject: [PATCH 09/10] feat: make api token creation idempotent --- .../dialogs/user-settings-dialog.svelte | 7 ++- .../src/lib/remote/api-tokens.remote.ts | 48 +++++++++++++------ 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte index dbe46ba9..936948b4 100644 --- a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte +++ b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte @@ -458,7 +458,12 @@ tokens.push({ id: tempId, name, created: now, lastUsedAt: null }); try { - const result = await createApiToken({ name }); + const request = await prepareIdempotentRequest('apiTokens.createApiToken', { + name, + userId: page.data.user?.id + }); + const result = await createApiToken({ name, idempotencyKey: request.key }); + request.complete(); generatedToken = result.token; const idx = tokens.findIndex((t) => t.id === tempId); if (idx !== -1) { diff --git a/apps/dashboard/src/lib/remote/api-tokens.remote.ts b/apps/dashboard/src/lib/remote/api-tokens.remote.ts index a25fbf83..5eed9a10 100644 --- a/apps/dashboard/src/lib/remote/api-tokens.remote.ts +++ b/apps/dashboard/src/lib/remote/api-tokens.remote.ts @@ -3,6 +3,7 @@ import { type } from 'arktype'; import { and, eq } from 'drizzle-orm'; import { initDrizzle } from '#lib/server/db/index.js'; import { apiTokens } from '#lib/server/db/schema.js'; +import { withIdempotency } from '#lib/server/idempotency.js'; import { captureServerEvent } from '#lib/server/posthog.js'; import { command, getRequestEvent, query } from '$app/server'; @@ -39,7 +40,7 @@ async function hashToken(token: string): Promise { return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); } -const createParams = type({ name: 'string' }); +const createParams = type({ name: 'string', idempotencyKey: '0 < string <= 128' }); export const createApiToken = command(createParams, async (params) => { const event = getRequestEvent(); @@ -48,22 +49,41 @@ export const createApiToken = command(createParams, async (params) => { } const db = initDrizzle(); + const userId = event.locals.user.id; const plainToken = generateToken(); const tokenHash = await hashToken(plainToken); + let insertedNow = false; + + const { id } = await withIdempotency( + db, + { + key: params.idempotencyKey, + operation: 'apiTokens.createApiToken', + args: { name: params.name, userId } + }, + async () => { + const [inserted] = await db + .insert(apiTokens) + .values({ userId, name: params.name, tokenHash, createdAt: Date.now() }) + .returning(); + insertedNow = true; + captureServerEvent('api_token_created', { token_id: inserted.id }); + return { id: inserted.id }; + } + ); + + if (!insertedNow) { + const [rotated] = await db + .update(apiTokens) + .set({ tokenHash }) + .where(and(eq(apiTokens.id, id), eq(apiTokens.userId, userId))) + .returning({ id: apiTokens.id }); + if (!rotated) { + error(409, 'This token was revoked. Create a new one.'); + } + } - const [inserted] = await db - .insert(apiTokens) - .values({ - userId: event.locals.user.id, - name: params.name, - tokenHash, - createdAt: Date.now() - }) - .returning(); - - captureServerEvent('api_token_created', { token_id: inserted.id }); - - return { id: inserted.id, token: plainToken }; + return { id, token: plainToken }; }); const revokeParams = type({ tokenId: 'string' }); From 2a5dc52abc1624df0714e52bae6c23649cca81ea Mon Sep 17 00:00:00 2001 From: Addison LeClair Date: Thu, 15 Oct 2026 04:55:35 +0000 Subject: [PATCH 10/10] refactor: centralize idempotency key handling in idempotent helper - Add idempotent() and idempotencyScope() to lib/idempotency.ts, replacing prepareIdempotentRequest - Derive userId from page state inside the helper so call sites no longer pass it - Remove manual request.complete() calls across create dialogs, pages, and admin campaign sends - Release the stored key as soon as the command resolves, not after navigation or redirect --- .../lib/components/buy-credits-dialog.svelte | 13 ++---- .../dialogs/user-settings-dialog.svelte | 16 ++----- apps/dashboard/src/lib/idempotency.ts | 42 +++++++++++++++---- apps/dashboard/src/routes/(app)/+page.svelte | 9 +--- .../routes/(app)/admin/emails/+page.svelte | 19 ++------- .../(app)/admin/projects/[[id]]/+page.svelte | 16 ++----- .../[projectid]/servers/create/+page.svelte | 13 ++---- 7 files changed, 55 insertions(+), 73 deletions(-) diff --git a/apps/dashboard/src/lib/components/buy-credits-dialog.svelte b/apps/dashboard/src/lib/components/buy-credits-dialog.svelte index 0bcfcdf9..44f3d411 100644 --- a/apps/dashboard/src/lib/components/buy-credits-dialog.svelte +++ b/apps/dashboard/src/lib/components/buy-credits-dialog.svelte @@ -3,7 +3,7 @@ import * as Dialog from '#lib/components/ui/dialog/index.js'; import { Input } from '#lib/components/ui/input/index.js'; import { Label } from '#lib/components/ui/label/index.js'; - import { prepareIdempotentRequest } from '#lib/idempotency.js'; + import { idempotent } from '#lib/idempotency.js'; import { purchaseCredits } from '#lib/remote/billing.remote.js'; import { getErrorMessage } from '#lib/utils.js'; import { page } from '$app/state'; @@ -35,7 +35,7 @@ let loading = $state(false); let actionError = $state(''); let amountInput = $state('10'); - let purchaseResult = $state<{ url: string | null; complete: () => void } | null>(null); + let purchaseResult = $state<{ url: string | null } | null>(null); const amount = $derived(Number.parseFloat(amountInput)); const amountValid = $derived(Number.isFinite(amount) && amount > 0); @@ -66,21 +66,16 @@ actionError = ''; try { if (!purchaseResult) { - const request = await prepareIdempotentRequest('billing.purchaseCredits', { + purchaseResult = await idempotent(purchaseCredits, 'billing.purchaseCredits', { projectId, - credits, - userId: page.data.user?.id + credits }); - const result = await purchaseCredits({ projectId, credits, idempotencyKey: request.key }); - purchaseResult = { ...result, complete: request.complete }; } if (purchaseResult.url) { window.location.href = purchaseResult.url; - purchaseResult.complete(); return; } window.location.reload(); - purchaseResult.complete(); } catch (err) { actionError = purchaseResult ? 'Your purchase was submitted. Select Continue to open the payment page or refresh your balance.' diff --git a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte index 936948b4..918a9e1c 100644 --- a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte +++ b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte @@ -12,7 +12,7 @@ import { Skeleton } from '#lib/components/ui/skeleton/index.js'; import * as Tabs from '#lib/components/ui/tabs/index.js'; import { confirmDestructive } from '#lib/confirm.svelte.js'; - import { prepareIdempotentRequest } from '#lib/idempotency.js'; + import { idempotent } from '#lib/idempotency.js'; import { createApiToken, listApiTokens, revokeApiToken } from '#lib/remote/api-tokens.remote.js'; import { getPendingEmailChange, requestEmailChange } from '#lib/remote/email-change.remote.js'; import { hasPassword } from '#lib/remote/password-change.remote.js'; @@ -361,14 +361,9 @@ sshKeyAdding = true; try { const payload = { name: newKeyName.trim(), publicKey: newKeyValue.trim() }; - const request = await prepareIdempotentRequest('sshKeys.createSshKey', { - ...payload, - userId: page.data.user?.id - }); - const res = await createSshKeyRpc({ ...payload, idempotencyKey: request.key }); + const res = await idempotent(createSshKeyRpc, 'sshKeys.createSshKey', payload); await invalidate('app:ssh-keys'); - request.complete(); sshKeys.push({ id: res.id, @@ -458,12 +453,7 @@ tokens.push({ id: tempId, name, created: now, lastUsedAt: null }); try { - const request = await prepareIdempotentRequest('apiTokens.createApiToken', { - name, - userId: page.data.user?.id - }); - const result = await createApiToken({ name, idempotencyKey: request.key }); - request.complete(); + const result = await idempotent(createApiToken, 'apiTokens.createApiToken', { name }); generatedToken = result.token; const idx = tokens.findIndex((t) => t.id === tempId); if (idx !== -1) { diff --git a/apps/dashboard/src/lib/idempotency.ts b/apps/dashboard/src/lib/idempotency.ts index 01487862..bd81d1d9 100644 --- a/apps/dashboard/src/lib/idempotency.ts +++ b/apps/dashboard/src/lib/idempotency.ts @@ -1,10 +1,13 @@ import { ulid } from 'ulid'; +import { page } from '$app/state'; export type RequestDetails = Record< string, string | number | boolean | null | undefined | readonly string[] >; +type IdempotentCommand = (input: I) => Promise; + export async function requestFingerprint(operation: string, details: RequestDetails) { const entries = Object.entries(details) .filter(([, value]) => value !== undefined) @@ -14,16 +17,41 @@ export async function requestFingerprint(operation: string, details: RequestDeta return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, '0')).join(''); } -export async function prepareIdempotentRequest(operation: string, details: RequestDetails) { - const storageKey = `request:${await requestFingerprint(operation, details)}`; - const key = sessionStorage.getItem(storageKey) ?? ulid(); - sessionStorage.setItem(storageKey, key); +export function idempotencyScope() { + const claimed = new Map(); return { - key, + async run( + command: IdempotentCommand, + operation: string, + input: Omit + ) { + const storageKey = `request:${await requestFingerprint(operation, { + input: JSON.stringify(input), + userId: page.data.user?.id + })}`; + const key = sessionStorage.getItem(storageKey) ?? ulid(); + sessionStorage.setItem(storageKey, key); + claimed.set(storageKey, key); + return command({ ...input, idempotencyKey: key } as I); + }, complete() { - if (sessionStorage.getItem(storageKey) === key) { - sessionStorage.removeItem(storageKey); + for (const [storageKey, key] of claimed) { + if (sessionStorage.getItem(storageKey) === key) { + sessionStorage.removeItem(storageKey); + } } + claimed.clear(); } }; } + +export async function idempotent( + command: IdempotentCommand, + operation: string, + input: Omit +) { + const scope = idempotencyScope(); + const result = await scope.run(command, operation, input); + scope.complete(); + return result; +} diff --git a/apps/dashboard/src/routes/(app)/+page.svelte b/apps/dashboard/src/routes/(app)/+page.svelte index 17dba84d..5b77185b 100644 --- a/apps/dashboard/src/routes/(app)/+page.svelte +++ b/apps/dashboard/src/routes/(app)/+page.svelte @@ -7,7 +7,7 @@ import * as DropdownMenu from '#lib/components/ui/dropdown-menu/index.js'; import { Input } from '#lib/components/ui/input/index.js'; import { Label } from '#lib/components/ui/label/index.js'; - import { prepareIdempotentRequest } from '#lib/idempotency.js'; + import { idempotent } from '#lib/idempotency.js'; import { createProject as createProjectRpc, deleteProject as deleteProjectRpc @@ -51,12 +51,7 @@ createProjectError = ''; try { const name = newProjectName.trim(); - const request = await prepareIdempotentRequest('projects.createProject', { - name, - userId: page.data.user?.id - }); - const res = await createProjectRpc({ name, idempotencyKey: request.key }); - request.complete(); + const res = await idempotent(createProjectRpc, 'projects.createProject', { name }); projects = [...projects, { id: res.id, projectName: name, role: 'owner' }]; newProjectName = ''; createOpen = false; diff --git a/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte b/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte index 375593e0..d9f33042 100644 --- a/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte +++ b/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte @@ -11,7 +11,7 @@ extractPlaceholders, fieldToken } from '#lib/emails/campaign-registry.js'; - import { prepareIdempotentRequest } from '#lib/idempotency.js'; + import { idempotencyScope } from '#lib/idempotency.js'; import type { AdminUser } from '#lib/remote/admin-users.remote.js'; import { previewCampaignEmail, @@ -392,7 +392,7 @@ sendError = ''; const rows = $state.snapshot(recipientRows); const fields = $state.snapshot(fieldValues); - const sentBatches: (() => void)[] = []; + const campaign = idempotencyScope(); try { for (let i = 0; i < rows.length; i += CAMPAIGN_BATCH_SIZE) { const batch = { @@ -403,22 +403,11 @@ emailColumn: 'email' }; // biome-ignore lint/performance/noAwaitInLoops: batches go out one at a time to stay under the email provider's rate limit and report progress - const request = await prepareIdempotentRequest('admin.sendCampaignEmails', { - template: batch.template, - subject: batch.subject, - fields: JSON.stringify(batch.fields), - rows: JSON.stringify(batch.rows), - emailColumn: batch.emailColumn, - userId: page.data.user?.id - }); - const result = await sendCampaignEmails({ ...batch, idempotencyKey: request.key }); - sentBatches.push(request.complete); + const result = await campaign.run(sendCampaignEmails, 'admin.sendCampaignEmails', batch); sentCount += result.sent; sendFailures = [...sendFailures, ...result.failures]; } - for (const complete of sentBatches) { - complete(); - } + campaign.complete(); sendComplete = true; } catch (err) { sendError = getErrorMessage(err, 'Failed to send emails'); diff --git a/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte b/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte index bca64447..875d4a19 100644 --- a/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte +++ b/apps/dashboard/src/routes/(app)/admin/projects/[[id]]/+page.svelte @@ -12,7 +12,7 @@ import * as Sheet from '#lib/components/ui/sheet/index.js'; import { Switch } from '#lib/components/ui/switch/index.js'; import { confirmDestructive } from '#lib/confirm.svelte.js'; - import { prepareIdempotentRequest } from '#lib/idempotency.js'; + import { idempotent } from '#lib/idempotency.js'; import { generateServerName } from '#lib/name-generator.js'; import { type AdminProject, @@ -248,13 +248,8 @@ createProjectError = ''; try { const payload = { name: createProjectName.trim(), ownerUserId: createProjectOwnerId }; - const request = await prepareIdempotentRequest('admin.createProject', { - ...payload, - userId: page.data.user?.id - }); - const created = await createAdminProject({ ...payload, idempotencyKey: request.key }); + const created = await idempotent(createAdminProject, 'admin.createProject', payload); await invalidate('app:admin-projects'); - request.complete(); createProjectOpen = false; toast.success('Project created'); goto(resolve(`admin/projects/${created.id}`), { reset: false }); @@ -335,13 +330,8 @@ ...(createVmSshKeyIds.length > 0 ? { sshKeyIds: [...createVmSshKeyIds].sort() } : {}), ...(createVmPassword.trim() ? { password: createVmPassword.trim() } : {}) }; - const request = await prepareIdempotentRequest('admin.createVm', { - ...payload, - userId: page.data.user?.id - }); - await createAdminVm({ ...payload, idempotencyKey: request.key }); + await idempotent(createAdminVm, 'admin.createVm', payload); await Promise.all([invalidate('app:admin-vms'), invalidate('app:admin-projects')]); - request.complete(); createVmOpen = false; toast.success('Server created'); } catch (err) { diff --git a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/create/+page.svelte b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/create/+page.svelte index 875ab012..ba528128 100644 --- a/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/create/+page.svelte +++ b/apps/dashboard/src/routes/(app)/projects/[projectid]/servers/create/+page.svelte @@ -5,7 +5,7 @@ import { Input } from '#lib/components/ui/input/index.js'; import * as Tabs from '#lib/components/ui/tabs/index.js'; import type { FeatureFlags } from '#lib/feature-flags.js'; - import { prepareIdempotentRequest } from '#lib/idempotency.js'; + import { idempotent } from '#lib/idempotency.js'; import { generateServerName } from '#lib/name-generator.js'; import { createVm } from '#lib/remote/vms.remote.js'; import { createVolume as createProjectVolume } from '#lib/remote/volumes.remote.js'; @@ -161,7 +161,7 @@ let creatingVolume = $state(false); let createError = $state(''); let billingSetupOpen = $state(false); - let createdVm = $state<{ id: string; projectId: string; complete: () => void } | null>(null); + let createdVm = $state<{ id: string; projectId: string } | null>(null); let selectedImage = $derived(dbImages.find((i) => i.id === selectedImageId) ?? null); let selectedPlan = $derived(vmTypes.find((t) => t.id === selectedPlanId) ?? null); @@ -397,7 +397,6 @@ await goto(resolve(`projects/${createdVm.projectId}/servers/${createdVm.id}`), { invalidate: ['project:vms'] }); - createdVm.complete(); requestServerStatusRefresh(); } catch { createError = @@ -448,12 +447,8 @@ ...(selectedSshKeyIds.length > 0 ? { sshKeyIds: [...selectedSshKeyIds].sort() } : {}), ...(usePasswordAuthentication ? { password: serverPassword.trim() } : {}) }; - const request = await prepareIdempotentRequest('vms.createVm', { - ...payload, - userId: page.data.user?.id - }); - const created = await createVm({ ...payload, idempotencyKey: request.key }); - createdVm = { id: created.id, projectId, complete: request.complete }; + const created = await idempotent(createVm, 'vms.createVm', payload); + createdVm = { id: created.id, projectId }; await openCreatedVm(); } catch (err) { createError = getErrorMessage(