diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..258943d
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ postgresql
+ true
+ org.postgresql.Driver
+ jdbc:postgresql://localhost:5400/trakqr
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/src/api/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.Designer.cs b/src/api/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.Designer.cs
new file mode 100644
index 0000000..465e112
--- /dev/null
+++ b/src/api/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.Designer.cs
@@ -0,0 +1,540 @@
+//
+using System;
+using Api.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace api.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260127193159_AddShortLinksQRCodesEventsAssets")]
+ partial class AddShortLinksQRCodesEventsAssets
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.2")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Api.Models.Asset", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("Mime")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Size")
+ .HasColumnType("bigint");
+
+ b.Property("StorageKey")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("WorkspaceId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("WorkspaceId");
+
+ b.ToTable("Assets");
+ });
+
+ modelBuilder.Entity("Api.Models.Domain", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("Hostname")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("VerificationToken")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("WorkspaceId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Hostname")
+ .IsUnique();
+
+ b.HasIndex("WorkspaceId");
+
+ b.ToTable("Domains");
+ });
+
+ modelBuilder.Entity("Api.Models.Event", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CountryCode")
+ .HasMaxLength(2)
+ .HasColumnType("character varying(2)");
+
+ b.Property("DedupeKey")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("DeviceType")
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("IpHash")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("QRCodeId")
+ .HasColumnType("uuid");
+
+ b.Property("Referrer")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("ShortLinkId")
+ .HasColumnType("uuid");
+
+ b.Property("Timestamp")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)");
+
+ b.Property("UserAgent")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("WorkspaceId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("QRCodeId");
+
+ b.HasIndex("Timestamp");
+
+ b.HasIndex("ShortLinkId", "Timestamp");
+
+ b.HasIndex("WorkspaceId", "Timestamp");
+
+ b.ToTable("Events");
+ });
+
+ modelBuilder.Entity("Api.Models.Project", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("WorkspaceId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("WorkspaceId");
+
+ b.ToTable("Projects");
+ });
+
+ modelBuilder.Entity("Api.Models.QRCodeDesign", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("LogoAssetId")
+ .HasColumnType("uuid");
+
+ b.Property("ProjectId")
+ .HasColumnType("uuid");
+
+ b.Property("ShortLinkId")
+ .HasColumnType("uuid");
+
+ b.Property("StyleJson")
+ .IsRequired()
+ .HasColumnType("jsonb");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("WorkspaceId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LogoAssetId");
+
+ b.HasIndex("ProjectId");
+
+ b.HasIndex("ShortLinkId");
+
+ b.HasIndex("WorkspaceId");
+
+ b.ToTable("QRCodeDesigns");
+ });
+
+ modelBuilder.Entity("Api.Models.ShortLink", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("DestinationUrl")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("DomainId")
+ .HasColumnType("uuid");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("PasswordHash")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("ProjectId")
+ .HasColumnType("uuid");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.Property("Title")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("WorkspaceId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProjectId");
+
+ b.HasIndex("WorkspaceId");
+
+ b.HasIndex("DomainId", "Slug")
+ .IsUnique();
+
+ b.ToTable("ShortLinks");
+ });
+
+ modelBuilder.Entity("Api.Models.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("PasswordHash")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("VerifiedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Email")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Api.Models.Workspace", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("OwnerUserId")
+ .HasColumnType("uuid");
+
+ b.Property("Plan")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerUserId");
+
+ b.ToTable("Workspaces");
+ });
+
+ modelBuilder.Entity("Api.Models.Asset", b =>
+ {
+ b.HasOne("Api.Models.Workspace", "Workspace")
+ .WithMany("Assets")
+ .HasForeignKey("WorkspaceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Workspace");
+ });
+
+ modelBuilder.Entity("Api.Models.Domain", b =>
+ {
+ b.HasOne("Api.Models.Workspace", "Workspace")
+ .WithMany("Domains")
+ .HasForeignKey("WorkspaceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Workspace");
+ });
+
+ modelBuilder.Entity("Api.Models.Event", b =>
+ {
+ b.HasOne("Api.Models.QRCodeDesign", "QRCode")
+ .WithMany("Events")
+ .HasForeignKey("QRCodeId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Api.Models.ShortLink", "ShortLink")
+ .WithMany("Events")
+ .HasForeignKey("ShortLinkId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Api.Models.Workspace", "Workspace")
+ .WithMany("Events")
+ .HasForeignKey("WorkspaceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("QRCode");
+
+ b.Navigation("ShortLink");
+
+ b.Navigation("Workspace");
+ });
+
+ modelBuilder.Entity("Api.Models.Project", b =>
+ {
+ b.HasOne("Api.Models.Workspace", "Workspace")
+ .WithMany("Projects")
+ .HasForeignKey("WorkspaceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Workspace");
+ });
+
+ modelBuilder.Entity("Api.Models.QRCodeDesign", b =>
+ {
+ b.HasOne("Api.Models.Asset", "LogoAsset")
+ .WithMany()
+ .HasForeignKey("LogoAssetId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Api.Models.Project", "Project")
+ .WithMany("QRCodeDesigns")
+ .HasForeignKey("ProjectId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Api.Models.ShortLink", "ShortLink")
+ .WithMany("QRCodeDesigns")
+ .HasForeignKey("ShortLinkId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Api.Models.Workspace", "Workspace")
+ .WithMany("QRCodeDesigns")
+ .HasForeignKey("WorkspaceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("LogoAsset");
+
+ b.Navigation("Project");
+
+ b.Navigation("ShortLink");
+
+ b.Navigation("Workspace");
+ });
+
+ modelBuilder.Entity("Api.Models.ShortLink", b =>
+ {
+ b.HasOne("Api.Models.Domain", "Domain")
+ .WithMany("ShortLinks")
+ .HasForeignKey("DomainId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Api.Models.Project", "Project")
+ .WithMany("ShortLinks")
+ .HasForeignKey("ProjectId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("Api.Models.Workspace", "Workspace")
+ .WithMany("ShortLinks")
+ .HasForeignKey("WorkspaceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Domain");
+
+ b.Navigation("Project");
+
+ b.Navigation("Workspace");
+ });
+
+ modelBuilder.Entity("Api.Models.Workspace", b =>
+ {
+ b.HasOne("Api.Models.User", "Owner")
+ .WithMany("Workspaces")
+ .HasForeignKey("OwnerUserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("Api.Models.Domain", b =>
+ {
+ b.Navigation("ShortLinks");
+ });
+
+ modelBuilder.Entity("Api.Models.Project", b =>
+ {
+ b.Navigation("QRCodeDesigns");
+
+ b.Navigation("ShortLinks");
+ });
+
+ modelBuilder.Entity("Api.Models.QRCodeDesign", b =>
+ {
+ b.Navigation("Events");
+ });
+
+ modelBuilder.Entity("Api.Models.ShortLink", b =>
+ {
+ b.Navigation("Events");
+
+ b.Navigation("QRCodeDesigns");
+ });
+
+ modelBuilder.Entity("Api.Models.User", b =>
+ {
+ b.Navigation("Workspaces");
+ });
+
+ modelBuilder.Entity("Api.Models.Workspace", b =>
+ {
+ b.Navigation("Assets");
+
+ b.Navigation("Domains");
+
+ b.Navigation("Events");
+
+ b.Navigation("Projects");
+
+ b.Navigation("QRCodeDesigns");
+
+ b.Navigation("ShortLinks");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/api/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.cs b/src/api/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.cs
new file mode 100644
index 0000000..0009643
--- /dev/null
+++ b/src/api/Migrations/20260127193159_AddShortLinksQRCodesEventsAssets.cs
@@ -0,0 +1,239 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace api.Migrations
+{
+ ///
+ public partial class AddShortLinksQRCodesEventsAssets : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Assets",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ WorkspaceId = table.Column(type: "uuid", nullable: false),
+ Type = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ StorageKey = table.Column(type: "character varying(512)", maxLength: 512, nullable: false),
+ Mime = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ Size = table.Column(type: "bigint", nullable: false),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Assets", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Assets_Workspaces_WorkspaceId",
+ column: x => x.WorkspaceId,
+ principalTable: "Workspaces",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "ShortLinks",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ WorkspaceId = table.Column(type: "uuid", nullable: false),
+ ProjectId = table.Column(type: "uuid", nullable: true),
+ DomainId = table.Column(type: "uuid", nullable: true),
+ Slug = table.Column(type: "character varying(50)", maxLength: 50, nullable: false),
+ DestinationUrl = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false),
+ Title = table.Column(type: "character varying(255)", maxLength: 255, nullable: true),
+ Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false),
+ ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true),
+ PasswordHash = table.Column(type: "character varying(255)", maxLength: 255, nullable: true),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
+ UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ShortLinks", x => x.Id);
+ table.ForeignKey(
+ name: "FK_ShortLinks_Domains_DomainId",
+ column: x => x.DomainId,
+ principalTable: "Domains",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ table.ForeignKey(
+ name: "FK_ShortLinks_Projects_ProjectId",
+ column: x => x.ProjectId,
+ principalTable: "Projects",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ table.ForeignKey(
+ name: "FK_ShortLinks_Workspaces_WorkspaceId",
+ column: x => x.WorkspaceId,
+ principalTable: "Workspaces",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "QRCodeDesigns",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ WorkspaceId = table.Column(type: "uuid", nullable: false),
+ ProjectId = table.Column(type: "uuid", nullable: true),
+ ShortLinkId = table.Column(type: "uuid", nullable: true),
+ StyleJson = table.Column(type: "jsonb", nullable: false),
+ LogoAssetId = table.Column(type: "uuid", nullable: true),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
+ UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_QRCodeDesigns", x => x.Id);
+ table.ForeignKey(
+ name: "FK_QRCodeDesigns_Assets_LogoAssetId",
+ column: x => x.LogoAssetId,
+ principalTable: "Assets",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ table.ForeignKey(
+ name: "FK_QRCodeDesigns_Projects_ProjectId",
+ column: x => x.ProjectId,
+ principalTable: "Projects",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ table.ForeignKey(
+ name: "FK_QRCodeDesigns_ShortLinks_ShortLinkId",
+ column: x => x.ShortLinkId,
+ principalTable: "ShortLinks",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ table.ForeignKey(
+ name: "FK_QRCodeDesigns_Workspaces_WorkspaceId",
+ column: x => x.WorkspaceId,
+ principalTable: "Workspaces",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Events",
+ columns: table => new
+ {
+ Id = table.Column(type: "bigint", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ WorkspaceId = table.Column(type: "uuid", nullable: false),
+ ShortLinkId = table.Column(type: "uuid", nullable: false),
+ QRCodeId = table.Column(type: "uuid", nullable: true),
+ Type = table.Column(type: "character varying(10)", maxLength: 10, nullable: false),
+ Timestamp = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
+ IpHash = table.Column(type: "character varying(64)", maxLength: 64, nullable: true),
+ UserAgent = table.Column(type: "character varying(512)", maxLength: 512, nullable: true),
+ Referrer = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: true),
+ CountryCode = table.Column(type: "character varying(2)", maxLength: 2, nullable: true),
+ DeviceType = table.Column(type: "character varying(20)", maxLength: 20, nullable: true),
+ DedupeKey = table.Column(type: "character varying(128)", maxLength: 128, nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Events", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Events_QRCodeDesigns_QRCodeId",
+ column: x => x.QRCodeId,
+ principalTable: "QRCodeDesigns",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.SetNull);
+ table.ForeignKey(
+ name: "FK_Events_ShortLinks_ShortLinkId",
+ column: x => x.ShortLinkId,
+ principalTable: "ShortLinks",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_Events_Workspaces_WorkspaceId",
+ column: x => x.WorkspaceId,
+ principalTable: "Workspaces",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Assets_WorkspaceId",
+ table: "Assets",
+ column: "WorkspaceId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Events_QRCodeId",
+ table: "Events",
+ column: "QRCodeId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Events_ShortLinkId_Timestamp",
+ table: "Events",
+ columns: new[] { "ShortLinkId", "Timestamp" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Events_Timestamp",
+ table: "Events",
+ column: "Timestamp");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Events_WorkspaceId_Timestamp",
+ table: "Events",
+ columns: new[] { "WorkspaceId", "Timestamp" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_QRCodeDesigns_LogoAssetId",
+ table: "QRCodeDesigns",
+ column: "LogoAssetId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_QRCodeDesigns_ProjectId",
+ table: "QRCodeDesigns",
+ column: "ProjectId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_QRCodeDesigns_ShortLinkId",
+ table: "QRCodeDesigns",
+ column: "ShortLinkId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_QRCodeDesigns_WorkspaceId",
+ table: "QRCodeDesigns",
+ column: "WorkspaceId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ShortLinks_DomainId_Slug",
+ table: "ShortLinks",
+ columns: new[] { "DomainId", "Slug" },
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ShortLinks_ProjectId",
+ table: "ShortLinks",
+ column: "ProjectId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ShortLinks_WorkspaceId",
+ table: "ShortLinks",
+ column: "WorkspaceId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Events");
+
+ migrationBuilder.DropTable(
+ name: "QRCodeDesigns");
+
+ migrationBuilder.DropTable(
+ name: "Assets");
+
+ migrationBuilder.DropTable(
+ name: "ShortLinks");
+ }
+ }
+}
diff --git a/src/api/Models/Asset.cs b/src/api/Models/Asset.cs
new file mode 100644
index 0000000..b7dc7e9
--- /dev/null
+++ b/src/api/Models/Asset.cs
@@ -0,0 +1,20 @@
+namespace Api.Models;
+
+public enum AssetType
+{
+ Logo
+}
+
+public class Asset
+{
+ public Guid Id { get; set; }
+ public Guid WorkspaceId { get; set; }
+ public AssetType Type { get; set; }
+ public required string StorageKey { get; set; }
+ public required string Mime { get; set; }
+ public long Size { get; set; }
+ public DateTime CreatedAt { get; set; }
+
+ // Navigation properties
+ public Workspace Workspace { get; set; } = null!;
+}
diff --git a/src/api/Models/Event.cs b/src/api/Models/Event.cs
new file mode 100644
index 0000000..abb9e25
--- /dev/null
+++ b/src/api/Models/Event.cs
@@ -0,0 +1,28 @@
+namespace Api.Models;
+
+public enum EventType
+{
+ Click,
+ Scan
+}
+
+public class Event
+{
+ public long Id { get; set; }
+ public Guid WorkspaceId { get; set; }
+ public Guid ShortLinkId { get; set; }
+ public Guid? QRCodeId { get; set; }
+ public EventType Type { get; set; }
+ public DateTime Timestamp { get; set; }
+ public string? IpHash { get; set; }
+ public string? UserAgent { get; set; }
+ public string? Referrer { get; set; }
+ public string? CountryCode { get; set; }
+ public string? DeviceType { get; set; }
+ public string? DedupeKey { get; set; }
+
+ // Navigation properties
+ public Workspace Workspace { get; set; } = null!;
+ public ShortLink ShortLink { get; set; } = null!;
+ public QRCodeDesign? QRCode { get; set; }
+}
diff --git a/src/api/Models/QRCodeDesign.cs b/src/api/Models/QRCodeDesign.cs
new file mode 100644
index 0000000..f4ccc7f
--- /dev/null
+++ b/src/api/Models/QRCodeDesign.cs
@@ -0,0 +1,20 @@
+namespace Api.Models;
+
+public class QRCodeDesign
+{
+ public Guid Id { get; set; }
+ public Guid WorkspaceId { get; set; }
+ public Guid? ProjectId { get; set; }
+ public Guid? ShortLinkId { get; set; }
+ public required string StyleJson { get; set; }
+ public Guid? LogoAssetId { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public DateTime UpdatedAt { get; set; }
+
+ // Navigation properties
+ public Workspace Workspace { get; set; } = null!;
+ public Project? Project { get; set; }
+ public ShortLink? ShortLink { get; set; }
+ public Asset? LogoAsset { get; set; }
+ public ICollection Events { get; set; } = [];
+}
diff --git a/src/api/Models/ShortLink.cs b/src/api/Models/ShortLink.cs
new file mode 100644
index 0000000..80c064a
--- /dev/null
+++ b/src/api/Models/ShortLink.cs
@@ -0,0 +1,30 @@
+namespace Api.Models;
+
+public enum ShortLinkStatus
+{
+ Active,
+ Disabled
+}
+
+public class ShortLink
+{
+ public Guid Id { get; set; }
+ public Guid WorkspaceId { get; set; }
+ public Guid? ProjectId { get; set; }
+ public Guid? DomainId { get; set; }
+ public required string Slug { get; set; }
+ public required string DestinationUrl { get; set; }
+ public string? Title { get; set; }
+ public ShortLinkStatus Status { get; set; } = ShortLinkStatus.Active;
+ public DateTime? ExpiresAt { get; set; }
+ public string? PasswordHash { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public DateTime UpdatedAt { get; set; }
+
+ // Navigation properties
+ public Workspace Workspace { get; set; } = null!;
+ public Project? Project { get; set; }
+ public Domain? Domain { get; set; }
+ public ICollection QRCodeDesigns { get; set; } = [];
+ public ICollection Events { get; set; } = [];
+}