diff --git a/src/database/entities/AvatarDecoration.ts b/src/database/entities/AvatarDecoration.ts
new file mode 100644
index 000000000..dedc8f3e6
--- /dev/null
+++ b/src/database/entities/AvatarDecoration.ts
@@ -0,0 +1,63 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2026 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { AvatarDecorationData } from "@spacebar/schemas";
+import { BaseClass } from "./BaseClass";
+import { User } from "./User";
+
+@Entity({
+ name: "avatar_decorations",
+})
+export class AvatarDecorations extends BaseClass {
+ @Column({})
+ asset: string;
+
+ @Column({ default: false })
+ approved: boolean;
+
+ @Column({ nullable: true })
+ @RelationId((deco: AvatarDecorations) => deco.uploader)
+ @Index("IDX_avatar_decoration_uploader_id")
+ uploader_id: string;
+
+ @JoinColumn({ name: "uploader_id", foreignKeyConstraintName: "FK_avatar_decoration_uploader_id" })
+ @ManyToOne(() => User, { onDelete: "CASCADE" })
+ uploader: User;
+
+ // access controls
+ @Column({ default: false })
+ public: boolean; // anyone can use
+
+ @Column({ array: true, type: "int8" })
+ allowed_user_ids: string[];
+
+ @Column({ array: true, type: "int8" })
+ allowed_guild_ids: string[];
+
+ @Column({ array: true, type: "int8" })
+ allowed_role_ids: string[];
+
+ toJSON(): AvatarDecorationData {
+ return {
+ asset: this.asset,
+ sku_id: this.id,
+ expires_at: null,
+ } satisfies AvatarDecorationData;
+ }
+}
diff --git a/src/database/migration/postgres/1789832735103-AvatarDecorationsInitial.ts b/src/database/migration/postgres/1789832735103-AvatarDecorationsInitial.ts
new file mode 100644
index 000000000..f8b5973cb
--- /dev/null
+++ b/src/database/migration/postgres/1789832735103-AvatarDecorationsInitial.ts
@@ -0,0 +1,22 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class AvatarDecorationsInitial1789832735103 implements MigrationInterface {
+ name = "AvatarDecorationsInitial1789832735103";
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "FK_user_settings_index"`);
+ await queryRunner.query(
+ `CREATE TABLE "avatar_decorations" ("id" bigint NOT NULL, "asset" character varying NOT NULL, "approved" boolean NOT NULL DEFAULT false, "uploader_id" bigint, "public" boolean NOT NULL DEFAULT false, "allowed_user_ids" bigint array NOT NULL, "allowed_guild_ids" bigint array NOT NULL, "allowed_role_ids" bigint array NOT NULL, CONSTRAINT "PK_17e3f1b6431fe3f3eca9916f3f6" PRIMARY KEY ("id"))`,
+ );
+ await queryRunner.query(`CREATE INDEX "IDX_avatar_decoration_uploader_id" ON "avatar_decorations" ("uploader_id") `);
+ await queryRunner.query(
+ `ALTER TABLE "avatar_decorations" ADD CONSTRAINT "FK_avatar_decoration_uploader_id" FOREIGN KEY ("uploader_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "avatar_decorations" DROP CONSTRAINT "FK_avatar_decoration_uploader_id"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_avatar_decoration_uploader_id"`);
+ await queryRunner.query(`DROP TABLE "avatar_decorations"`);
+ }
+}