diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 05fb5d9..8836f7f 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -13,11 +13,19 @@ return new class extends Migration { Schema::create('users', function (Blueprint $table) { $table->id(); + $table->string('ulid', 26)->unique(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); + $table->string('two_factor_secret')->nullable(); + $table->text('two_factor_recovery')->nullable(); + $table->timestamp('two_factor_confirmed_at')->nullable(); + $table->string('locale', 5)->default('de'); + $table->enum('theme', ['dark', 'light', 'system'])->default('dark'); + $table->unsignedBigInteger('default_workspace_id')->nullable(); $table->rememberToken(); + $table->softDeletes(); $table->timestamps(); }); diff --git a/database/migrations/2026_05_15_299000_create_plans_table.php b/database/migrations/2026_05_15_299000_create_plans_table.php new file mode 100644 index 0000000..cdc5b0b --- /dev/null +++ b/database/migrations/2026_05_15_299000_create_plans_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('name', 32)->unique(); + $table->string('stripe_price_id')->nullable(); + $table->unsignedInteger('monthly_price_cents')->default(0); + $table->unsignedSmallInteger('workspace_limit')->default(1); + $table->unsignedSmallInteger('member_limit')->default(1); + $table->unsignedSmallInteger('custom_domain_limit')->default(0); + $table->unsignedInteger('link_limit')->default(50); + $table->unsignedBigInteger('click_limit_monthly')->default(1000); + $table->json('features')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('plans'); + } +}; diff --git a/database/migrations/2026_05_15_300000_create_workspaces_table.php b/database/migrations/2026_05_15_300000_create_workspaces_table.php new file mode 100644 index 0000000..5e04fa1 --- /dev/null +++ b/database/migrations/2026_05_15_300000_create_workspaces_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('ulid', 26)->unique(); + $table->string('name'); + $table->string('slug', 64); + $table->foreignId('owner_id')->constrained('users')->cascadeOnDelete(); + $table->foreignId('plan_id')->nullable()->constrained('plans')->nullOnDelete(); + $table->timestamp('trial_ends_at')->nullable(); + $table->json('branding')->nullable(); + $table->string('stripe_id')->nullable()->index(); + $table->json('stripe_customer_metadata')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('workspaces'); + } +}; diff --git a/database/migrations/2026_05_15_300001_create_workspace_members_table.php b/database/migrations/2026_05_15_300001_create_workspace_members_table.php new file mode 100644 index 0000000..d777be6 --- /dev/null +++ b/database/migrations/2026_05_15_300001_create_workspace_members_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->enum('role', ['owner', 'admin', 'editor', 'viewer'])->default('editor'); + $table->foreignId('invited_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamp('joined_at')->nullable(); + $table->timestamps(); + $table->unique(['workspace_id', 'user_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('workspace_members'); + } +}; diff --git a/database/migrations/2026_05_15_300002_create_workspace_invitations_table.php b/database/migrations/2026_05_15_300002_create_workspace_invitations_table.php new file mode 100644 index 0000000..2e4c936 --- /dev/null +++ b/database/migrations/2026_05_15_300002_create_workspace_invitations_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); + $table->string('email'); + $table->enum('role', ['admin', 'editor', 'viewer'])->default('editor'); + $table->string('token', 64)->unique(); + $table->foreignId('invited_by')->constrained('users')->cascadeOnDelete(); + $table->timestamp('expires_at'); + $table->timestamp('accepted_at')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('workspace_invitations'); + } +}; diff --git a/database/migrations/2026_05_15_300003_create_billing_profiles_table.php b/database/migrations/2026_05_15_300003_create_billing_profiles_table.php new file mode 100644 index 0000000..c03af10 --- /dev/null +++ b/database/migrations/2026_05_15_300003_create_billing_profiles_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('workspace_id')->unique()->constrained()->cascadeOnDelete(); + $table->foreignId('billing_user_id')->constrained('users')->cascadeOnDelete(); + $table->string('company_name')->nullable(); + $table->string('vat_number', 30)->nullable(); + $table->string('address_line_1'); + $table->string('address_line_2')->nullable(); + $table->string('city'); + $table->string('postal_code', 20); + $table->string('country', 2); + $table->string('email_billing'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('billing_profiles'); + } +}; diff --git a/database/migrations/2026_05_15_300010_create_domains_table.php b/database/migrations/2026_05_15_300010_create_domains_table.php new file mode 100644 index 0000000..dcf9a36 --- /dev/null +++ b/database/migrations/2026_05_15_300010_create_domains_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); + $table->string('hostname', 253)->unique(); + $table->enum('type', ['short', 'bio', 'both'])->default('short'); + $table->timestamp('verified_at')->nullable(); + $table->string('verification_token', 64)->nullable(); + $table->enum('ssl_status', ['pending', 'active', 'failed'])->default('pending'); + $table->timestamp('ssl_issued_at')->nullable(); + $table->timestamp('ssl_expires_at')->nullable(); + $table->boolean('is_default')->default(false); + $table->timestamps(); + $table->index(['workspace_id', 'hostname']); + }); + } + + public function down(): void + { + Schema::dropIfExists('domains'); + } +}; diff --git a/database/migrations/2026_05_15_300020_create_links_table.php b/database/migrations/2026_05_15_300020_create_links_table.php new file mode 100644 index 0000000..b43804a --- /dev/null +++ b/database/migrations/2026_05_15_300020_create_links_table.php @@ -0,0 +1,42 @@ +id(); + $table->string('ulid', 26)->unique(); + $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); + $table->foreignId('domain_id')->nullable()->constrained()->nullOnDelete(); + $table->string('slug', 64); + $table->text('target_url'); + $table->string('title')->nullable(); + $table->text('description')->nullable(); + $table->string('favicon_url')->nullable(); + $table->enum('status', ['active', 'disabled', 'expired'])->default('active'); + $table->string('password', 255)->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->unsignedBigInteger('click_limit')->nullable(); + $table->json('rules')->nullable(); + $table->json('pixel_config')->nullable(); + $table->json('tags')->nullable(); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); + $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete(); + $table->softDeletes(); + $table->timestamps(); + $table->unique(['domain_id', 'slug']); + $table->index('workspace_id'); + $table->index('status'); + }); + } + + public function down(): void + { + Schema::dropIfExists('links'); + } +}; diff --git a/database/migrations/2026_05_15_300021_create_link_variants_table.php b/database/migrations/2026_05_15_300021_create_link_variants_table.php new file mode 100644 index 0000000..5c18700 --- /dev/null +++ b/database/migrations/2026_05_15_300021_create_link_variants_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('link_id')->constrained()->cascadeOnDelete(); + $table->string('label', 64); + $table->text('target_url'); + $table->unsignedTinyInteger('weight')->default(50); + $table->unsignedBigInteger('clicks_count')->default(0); + $table->unsignedBigInteger('conversions_count')->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('link_variants'); + } +}; diff --git a/database/migrations/2026_05_15_300030_create_qr_codes_table.php b/database/migrations/2026_05_15_300030_create_qr_codes_table.php new file mode 100644 index 0000000..1f7f055 --- /dev/null +++ b/database/migrations/2026_05_15_300030_create_qr_codes_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('ulid', 26)->unique(); + $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); + $table->foreignId('link_id')->nullable()->constrained()->nullOnDelete(); + $table->enum('type', ['url', 'vcard', 'wifi', 'pdf', 'mp3', 'text'])->default('url'); + $table->json('payload'); + $table->json('style')->nullable(); + $table->boolean('is_dynamic')->default(true); + $table->unsignedBigInteger('scan_limit')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->softDeletes(); + $table->timestamps(); + $table->index('workspace_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('qr_codes'); + } +}; diff --git a/database/migrations/2026_05_15_300040_create_bio_pages_table.php b/database/migrations/2026_05_15_300040_create_bio_pages_table.php new file mode 100644 index 0000000..1e2f952 --- /dev/null +++ b/database/migrations/2026_05_15_300040_create_bio_pages_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('ulid', 26)->unique(); + $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); + $table->foreignId('domain_id')->nullable()->constrained()->nullOnDelete(); + $table->string('slug', 64); + $table->json('title'); + $table->json('description')->nullable(); + $table->string('og_image')->nullable(); + $table->json('theme')->nullable(); + $table->boolean('is_published')->default(false); + $table->softDeletes(); + $table->timestamps(); + $table->unique(['domain_id', 'slug']); + $table->index('workspace_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('bio_pages'); + } +}; diff --git a/database/migrations/2026_05_15_300041_create_bio_blocks_table.php b/database/migrations/2026_05_15_300041_create_bio_blocks_table.php new file mode 100644 index 0000000..b061b46 --- /dev/null +++ b/database/migrations/2026_05_15_300041_create_bio_blocks_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('bio_page_id')->constrained()->cascadeOnDelete(); + $table->unsignedSmallInteger('position')->default(0); + $table->string('type', 32); + $table->json('config'); + $table->timestamps(); + $table->index(['bio_page_id', 'position']); + }); + } + + public function down(): void + { + Schema::dropIfExists('bio_blocks'); + } +}; diff --git a/database/migrations/2026_05_15_300050_create_clicks_table.php b/database/migrations/2026_05_15_300050_create_clicks_table.php new file mode 100644 index 0000000..ba6eafe --- /dev/null +++ b/database/migrations/2026_05_15_300050_create_clicks_table.php @@ -0,0 +1,41 @@ +bigIncrements('id'); + $table->foreignId('link_id')->constrained()->cascadeOnDelete(); + $table->foreignId('variant_id')->nullable()->constrained('link_variants')->nullOnDelete(); + $table->unsignedBigInteger('workspace_id'); + $table->timestamp('clicked_at')->useCurrent(); + $table->string('ip_hash', 64); + $table->string('country', 2)->nullable(); + $table->string('city', 64)->nullable(); + $table->enum('device', ['desktop', 'mobile', 'tablet', 'bot', 'unknown'])->default('unknown'); + $table->string('os', 32)->nullable(); + $table->string('browser', 32)->nullable(); + $table->string('referrer_host', 253)->nullable(); + $table->string('referrer_path', 2048)->nullable(); + $table->string('utm_source', 128)->nullable(); + $table->string('utm_medium', 128)->nullable(); + $table->string('utm_campaign', 128)->nullable(); + $table->string('utm_content', 128)->nullable(); + $table->string('utm_term', 128)->nullable(); + $table->string('language', 10)->nullable(); + $table->index('clicked_at'); + $table->index(['link_id', 'clicked_at']); + $table->index(['workspace_id', 'clicked_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('clicks'); + } +}; diff --git a/database/migrations/2026_05_15_300051_create_clicks_hourly_table.php b/database/migrations/2026_05_15_300051_create_clicks_hourly_table.php new file mode 100644 index 0000000..d30fa68 --- /dev/null +++ b/database/migrations/2026_05_15_300051_create_clicks_hourly_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('link_id')->constrained()->cascadeOnDelete(); + $table->timestamp('hour'); + $table->string('country', 2)->nullable(); + $table->enum('device', ['desktop', 'mobile', 'tablet', 'bot', 'unknown'])->nullable(); + $table->unsignedBigInteger('count')->default(0); + $table->unsignedBigInteger('unique_visitors')->default(0); + $table->unique(['link_id', 'hour', 'country', 'device']); + $table->index(['link_id', 'hour']); + }); + } + + public function down(): void + { + Schema::dropIfExists('clicks_hourly'); + } +}; diff --git a/database/migrations/2026_05_15_300052_create_clicks_daily_table.php b/database/migrations/2026_05_15_300052_create_clicks_daily_table.php new file mode 100644 index 0000000..73b5845 --- /dev/null +++ b/database/migrations/2026_05_15_300052_create_clicks_daily_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('link_id')->constrained()->cascadeOnDelete(); + $table->date('day'); + $table->string('country', 2)->nullable(); + $table->enum('device', ['desktop', 'mobile', 'tablet', 'bot', 'unknown'])->nullable(); + $table->unsignedBigInteger('count')->default(0); + $table->unsignedBigInteger('unique_visitors')->default(0); + $table->unique(['link_id', 'day', 'country', 'device']); + $table->index(['link_id', 'day']); + }); + } + + public function down(): void + { + Schema::dropIfExists('clicks_daily'); + } +}; diff --git a/database/migrations/2026_05_15_300053_create_clicks_by_referrer_table.php b/database/migrations/2026_05_15_300053_create_clicks_by_referrer_table.php new file mode 100644 index 0000000..1c2e40a --- /dev/null +++ b/database/migrations/2026_05_15_300053_create_clicks_by_referrer_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('link_id')->constrained()->cascadeOnDelete(); + $table->date('day'); + $table->string('referrer_host', 253)->nullable(); + $table->unsignedBigInteger('count')->default(0); + $table->unique(['link_id', 'day', 'referrer_host']); + $table->index(['link_id', 'day']); + }); + } + + public function down(): void + { + Schema::dropIfExists('clicks_by_referrer'); + } +}; diff --git a/database/migrations/2026_05_15_300054_create_clicks_by_utm_table.php b/database/migrations/2026_05_15_300054_create_clicks_by_utm_table.php new file mode 100644 index 0000000..c36c104 --- /dev/null +++ b/database/migrations/2026_05_15_300054_create_clicks_by_utm_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('link_id')->constrained()->cascadeOnDelete(); + $table->date('day'); + $table->string('utm_source', 128)->nullable(); + $table->string('utm_medium', 128)->nullable(); + $table->string('utm_campaign', 128)->nullable(); + $table->unsignedBigInteger('count')->default(0); + $table->unique(['link_id', 'day', 'utm_source', 'utm_medium', 'utm_campaign'], 'clicks_by_utm_unique'); + $table->index(['link_id', 'day']); + }); + } + + public function down(): void + { + Schema::dropIfExists('clicks_by_utm'); + } +}; diff --git a/database/migrations/2026_05_15_300070_create_translations_table.php b/database/migrations/2026_05_15_300070_create_translations_table.php new file mode 100644 index 0000000..77e569a --- /dev/null +++ b/database/migrations/2026_05_15_300070_create_translations_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('locale', 5); + $table->string('namespace', 32)->default('*'); + $table->string('group', 64); + $table->string('key', 255); + $table->text('value'); + $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + $table->unique(['locale', 'namespace', 'group', 'key']); + $table->index(['locale', 'group']); + }); + } + + public function down(): void + { + Schema::dropIfExists('translations'); + } +}; diff --git a/database/migrations/2026_05_15_300080_create_webhooks_table.php b/database/migrations/2026_05_15_300080_create_webhooks_table.php new file mode 100644 index 0000000..ac547fe --- /dev/null +++ b/database/migrations/2026_05_15_300080_create_webhooks_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); + $table->string('url'); + $table->string('secret', 64); + $table->json('events'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + $table->index('workspace_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('webhooks'); + } +}; diff --git a/database/migrations/2026_05_15_300081_create_webhook_deliveries_table.php b/database/migrations/2026_05_15_300081_create_webhook_deliveries_table.php new file mode 100644 index 0000000..f502bd9 --- /dev/null +++ b/database/migrations/2026_05_15_300081_create_webhook_deliveries_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('webhook_id')->constrained()->cascadeOnDelete(); + $table->string('event', 64); + $table->text('payload'); + $table->enum('status', ['success', 'failed', 'pending'])->default('pending'); + $table->unsignedSmallInteger('response_code')->nullable(); + $table->unsignedTinyInteger('attempts')->default(1); + $table->timestamps(); + $table->index(['webhook_id', 'created_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('webhook_deliveries'); + } +};