feat(api): Add payments with Polar.sh (#264)

## Description
<!-- Briefly describe the purpose and scope of your changes -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a new subscription API endpoint for managing subscriptions
and products.
- Enhanced subscription management with new entities and
functionalities.
- Added functionality to retrieve current timestamps in both local and
UTC formats.
- Added Polar.sh integration with customer portal and checkout session
creation APIs.

- **Refactor**
- Redesigned team details to now present members and subscription
information instead of a plan type.
  - Enhanced member management by incorporating role assignments.
- Streamlined user data handling and removed legacy subscription event
logic.
  - Simplified error handling in actor functions for better clarity.
  - Updated plan types and UI labels to reflect new subscription tiers.
  - Improved database indexing for Steam user data.

- **Chores**
- Updated the database schema with new tables and fields to support
subscription, team, and member enhancements.
  - Extended identifier prefixes to broaden system integration.
- Added new secrets related to pricing plans in infrastructure
configuration.
  - Configured API and auth routing with new domain and routing rules.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Wanjohi
2025-04-18 14:24:19 +03:00
committed by GitHub
parent 76d27e4708
commit 47e61599bb
40 changed files with 3304 additions and 425 deletions

View File

@@ -6,15 +6,21 @@ import { cluster } from "./cluster";
import { postgres } from "./postgres";
export const api = new sst.aws.Service("Api", {
cluster,
cpu: $app.stage === "production" ? "2 vCPU" : undefined,
memory: $app.stage === "production" ? "4 GB" : undefined,
cluster,
command: ["bun", "run", "./src/api/index.ts"],
link: [
bus,
auth,
postgres,
secret.PolarSecret,
secret.PolarWebhookSecret,
secret.NestriFamilyMonthly,
secret.NestriFamilyYearly,
secret.NestriFreeMonthly,
secret.NestriProMonthly,
secret.NestriProYearly,
],
image: {
dockerfile: "packages/functions/Containerfile",
@@ -23,16 +29,11 @@ export const api = new sst.aws.Service("Api", {
NO_COLOR: "1",
},
loadBalancer: {
domain: "api." + domain,
rules: [
{
listen: "80/http",
forward: "3001/http",
},
{
listen: "443/https",
forward: "3001/http",
},
],
},
dev: {
@@ -47,4 +48,16 @@ export const api = new sst.aws.Service("Api", {
max: 10,
}
: undefined,
});
});
export const apiRoute = new sst.aws.Router("ApiRoute", {
routes: {
// I think api.url should work all the same
"/*": api.nodes.loadBalancer.dnsName,
},
domain: {
name: "api." + domain,
dns: sst.cloudflare.dns(),
},
})

View File

@@ -1,26 +1,14 @@
import { bus } from "./bus";
import { domain } from "./dns";
// import { email } from "./email";
import { secret } from "./secret";
import { postgres } from "./postgres";
import { cluster } from "./cluster";
// sst.Linkable.wrap(random.RandomString, (resource) => ({
// properties: {
// value: resource.result,
// },
// }));
// export const authFingerprintKey = new random.RandomString(
// "AuthFingerprintKey",
// {
// length: 32,
// },
// );
import { postgres } from "./postgres";
//FIXME: Use a shared /tmp folder
export const auth = new sst.aws.Service("Auth", {
cluster,
cpu: $app.stage === "production" ? "1 vCPU" : undefined,
memory: $app.stage === "production" ? "2 GB" : undefined,
cluster,
command: ["bun", "run", "./src/auth.ts"],
link: [
bus,
@@ -38,18 +26,12 @@ export const auth = new sst.aws.Service("Auth", {
NO_COLOR: "1",
STORAGE: $dev ? "/tmp/persist.json" : "/mnt/efs/persist.json"
},
//TODO: Use API gateway instead, because of the API headers
loadBalancer: {
domain: "auth." + domain,
rules: [
{
listen: "80/http",
forward: "3002/http",
},
{
listen: "443/https",
forward: "3002/http",
},
],
},
permissions: [
@@ -70,4 +52,15 @@ export const auth = new sst.aws.Service("Auth", {
max: 10,
}
: undefined,
});
});
export const authRoute = new sst.aws.Router("AuthRoute", {
routes: {
// I think auth.url should work all the same
"/*": auth.nodes.loadBalancer.dnsName,
},
domain: {
name: "auth." + domain,
dns: sst.cloudflare.dns(),
},
})

View File

@@ -1,11 +1,17 @@
export const secret = {
// InstantAppId: new sst.Secret("InstantAppId"),
PolarSecret: new sst.Secret("PolarSecret", process.env.POLAR_API_KEY),
GithubClientID: new sst.Secret("GithubClientID"),
DiscordClientID: new sst.Secret("DiscordClientID"),
PolarWebhookSecret: new sst.Secret("PolarWebhookSecret"),
GithubClientSecret: new sst.Secret("GithubClientSecret"),
// InstantAdminToken: new sst.Secret("InstantAdminToken"),
DiscordClientSecret: new sst.Secret("DiscordClientSecret"),
// Pricing
NestriFreeMonthly: new sst.Secret("NestriFreeMonthly"),
NestriProMonthly: new sst.Secret("NestriProMonthly"),
NestriProYearly: new sst.Secret("NestriProYearly"),
NestriFamilyMonthly: new sst.Secret("NestriFamilyMonthly"),
NestriFamilyYearly: new sst.Secret("NestriFamilyYearly"),
};
export const allSecrets = Object.values(secret);

View File

@@ -0,0 +1,2 @@
ALTER TABLE "member" ADD COLUMN "role" text NOT NULL;--> statement-breakpoint
ALTER TABLE "team" DROP COLUMN "plan_type";

View File

@@ -0,0 +1,15 @@
CREATE TABLE "subscription" (
"id" char(30) NOT NULL,
"user_id" char(30) NOT NULL,
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
"time_deleted" timestamp with time zone,
"team_id" char(30) NOT NULL,
"standing" text NOT NULL,
"plan_type" text NOT NULL,
"tokens" integer NOT NULL,
"product_id" varchar(255),
"subscription_id" varchar(255)
);
--> statement-breakpoint
ALTER TABLE "subscription" ADD CONSTRAINT "subscription_team_id_team_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."team"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "subscription" ADD CONSTRAINT "subscription_id_team_id_pk" PRIMARY KEY("id","team_id");--> statement-breakpoint
CREATE UNIQUE INDEX "subscription_id" ON "subscription" USING btree ("id");--> statement-breakpoint
CREATE INDEX "subscription_user_id" ON "subscription" USING btree ("user_id");

View File

@@ -0,0 +1,2 @@
CREATE UNIQUE INDEX "steam_id" ON "steam" USING btree ("steam_id");--> statement-breakpoint
CREATE INDEX "steam_user_id" ON "steam" USING btree ("user_id");

View File

@@ -0,0 +1,485 @@
{
"id": "69827225-1351-4709-a9b2-facb0f569215",
"prevId": "0b04858c-a7e3-43b6-98a4-1dc2f6f97488",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,580 @@
{
"id": "fff2b73d-85ab-48bc-86de-69d3caf317f0",
"prevId": "69827225-1351-4709-a9b2-facb0f569215",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.subscription": {
"name": "subscription",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"standing": {
"name": "standing",
"type": "text",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"tokens": {
"name": "tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"product_id": {
"name": "product_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"subscription_id": {
"name": "subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"subscription_team_id_team_id_fk": {
"name": "subscription_team_id_team_id_fk",
"tableFrom": "subscription",
"tableTo": "team",
"columnsFrom": [
"team_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,619 @@
{
"id": "17b9c14f-ff15-44a5-9aaf-3f3b7dd7d294",
"prevId": "fff2b73d-85ab-48bc-86de-69d3caf317f0",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.subscription": {
"name": "subscription",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"standing": {
"name": "standing",
"type": "text",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"tokens": {
"name": "tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"product_id": {
"name": "product_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"subscription_id": {
"name": "subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"subscription_id": {
"name": "subscription_id",
"columns": [
{
"expression": "id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"subscription_user_id": {
"name": "subscription_user_id",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"subscription_team_id_team_id_fk": {
"name": "subscription_team_id_team_id_fk",
"tableFrom": "subscription",
"tableTo": "team",
"columnsFrom": [
"team_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"subscription_id_team_id_pk": {
"name": "subscription_id_team_id_pk",
"columns": [
"id",
"team_id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -0,0 +1,650 @@
{
"id": "1717c769-cee0-4242-bcbb-9538c80d985c",
"prevId": "17b9c14f-ff15-44a5-9aaf-3f3b7dd7d294",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.machine": {
"name": "machine",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": true
},
"timezone": {
"name": "timezone",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "point",
"primaryKey": false,
"notNull": true
},
"fingerprint": {
"name": "fingerprint",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_fingerprint": {
"name": "machine_fingerprint",
"columns": [
{
"expression": "fingerprint",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.member": {
"name": "member",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true
},
"time_seen": {
"name": "time_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"email_global": {
"name": "email_global",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"member_email": {
"name": "member_email",
"columns": [
{
"expression": "team_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"member_team_id_id_pk": {
"name": "member_team_id_id_pk",
"columns": [
"team_id",
"id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.steam": {
"name": "steam",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"steam_id": {
"name": "steam_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"last_game": {
"name": "last_game",
"type": "json",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country_code": {
"name": "country_code",
"type": "varchar(2)",
"primaryKey": false,
"notNull": true
},
"steam_email": {
"name": "steam_email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"persona_name": {
"name": "persona_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"limitation": {
"name": "limitation",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"steam_id": {
"name": "steam_id",
"columns": [
{
"expression": "steam_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"steam_user_id": {
"name": "steam_user_id",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"steam_user_id_user_id_fk": {
"name": "steam_user_id_user_id_fk",
"tableFrom": "steam",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.subscription": {
"name": "subscription",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"team_id": {
"name": "team_id",
"type": "char(30)",
"primaryKey": false,
"notNull": true
},
"standing": {
"name": "standing",
"type": "text",
"primaryKey": false,
"notNull": true
},
"plan_type": {
"name": "plan_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"tokens": {
"name": "tokens",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"product_id": {
"name": "product_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"subscription_id": {
"name": "subscription_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"subscription_id": {
"name": "subscription_id",
"columns": [
{
"expression": "id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"subscription_user_id": {
"name": "subscription_user_id",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"subscription_team_id_team_id_fk": {
"name": "subscription_team_id_team_id_fk",
"tableFrom": "subscription",
"tableTo": "team",
"columnsFrom": [
"team_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"subscription_id_team_id_pk": {
"name": "subscription_id_team_id_pk",
"columns": [
"id",
"team_id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.team": {
"name": "team",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": [
{
"expression": "slug",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "char(30)",
"primaryKey": true,
"notNull": true
},
"time_created": {
"name": "time_created",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"avatar_url": {
"name": "avatar_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"discriminator": {
"name": "discriminator",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"polar_customer_id": {
"name": "polar_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": [
{
"expression": "email",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_polar_customer_id_unique": {
"name": "user_polar_customer_id_unique",
"nullsNotDistinct": false,
"columns": [
"polar_customer_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -43,6 +43,34 @@
"when": 1744614896792,
"tag": "0005_aspiring_stature",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1744634229644,
"tag": "0006_worthless_dreadnoughts",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1744634322996,
"tag": "0007_warm_secret_warriors",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1744651530530,
"tag": "0008_third_mindworm",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1744651817581,
"tag": "0009_luxuriant_wraith",
"breakpoints": true
}
]
}

View File

@@ -109,43 +109,34 @@ export function assertActor<T extends Actor["type"]>(type: T) {
return actor as Extract<Actor, { type: T }>;
}
/**
* Returns the current actor's team ID.
*
* @returns The team ID associated with the current actor.
* @throws {VisibleError} If the current actor does not have a {@link teamID} property.
*/
export function useTeam() {
const actor = useActor();
if ("teamID" in actor.properties) return actor.properties.teamID;
throw new Error(`Expected actor to have teamID`);
}
export function useMachine() {
const actor = useActor();
if ("machineID" in actor.properties) return actor.properties.fingerprint;
throw new Error(`Expected actor to have fingerprint`);
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`Expected actor to have teamID`
);
}
/**
* Asserts that the current user possesses the specified flag.
* Returns the fingerprint of the current actor if the actor has a machine identity.
*
* This function executes a database transaction that queries the user table for the current user's flags.
* If the flags are missing, it throws a {@link VisibleError} with the code {@link ErrorCodes.Validation.MISSING_REQUIRED_FIELD}
* and a message indicating that the required flag is absent.
*
* @param flag - The name of the user flag to verify.
*
* @throws {VisibleError} If the user's flag is missing.
* @returns The fingerprint of the current machine actor.
* @throws {VisibleError} If the current actor does not have a machine identity.
*/
export async function assertUserFlag(flag: keyof UserFlags) {
return useTransaction((tx) =>
tx
.select({ flags: userTable.flags })
.from(userTable)
.where(eq(userTable.id, useUserID()))
.then((rows) => {
const flags = rows[0]?.flags;
if (!flags)
throw new VisibleError(
"not_found",
ErrorCodes.Validation.MISSING_REQUIRED_FIELD,
"Actor does not have " + flag + " flag",
);
}),
export function useMachine() {
const actor = useActor();
if ("machineID" in actor.properties) return actor.properties.fingerprint;
throw new VisibleError(
"authentication",
ErrorCodes.Authentication.UNAUTHORIZED,
`Expected actor to have fingerprint`
);
}

View File

@@ -1,7 +1,11 @@
import { sql } from "drizzle-orm";
import { z } from "zod";
import "zod-openapi/extend";
export namespace Common {
export const IdDescription = `Unique object identifier.
The format and length of IDs may change over time.`;
export const now = () => sql`now()`;
export const utc = () => sql`now() at time zone 'utc'`;
}

View File

@@ -1,4 +1,5 @@
import { char, timestamp as rawTs } from "drizzle-orm/pg-core";
import { teamTable } from "../team/team.sql";
export const ulid = (name: string) => char(name, { length: 26 + 4 });

View File

@@ -34,23 +34,38 @@ export namespace Examples {
steamAccounts: [Steam]
};
export const Team = {
id: Id("team"),
name: "John Does' Team",
slug: "john_doe",
planType: "BYOG" as const
export const Product = {
id: Id("product"),
name: "RTX 4090",
description: "Ideal for dedicated gamers who crave more flexibility and social gaming experiences.",
tokensPerHour: 20,
}
export const Subscription = {
tokens: 100,
id: Id("subscription"),
userID: Id("user"),
teamID: Id("team"),
planType: "pro" as const, // free, pro, family, enterprise
standing: "new" as const, // new, good, overdue, cancelled
polarProductID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
polarSubscriptionID: "0bfcb712-df13-4454-81a8-fbee66eddca4",
}
export const Member = {
id: Id("member"),
email: "john@example.com",
teamID: Id("team"),
role: "admin" as const,
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
}
export const Polar = {
teamID: Id("team"),
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
export const Team = {
id: Id("team"),
name: "John Does' Team",
slug: "john_doe",
subscriptions: [Subscription],
members: [Member]
}
export const Machine = {

View File

@@ -6,7 +6,7 @@ import { Common } from "../common";
import { createID, fn } from "../utils";
import { createEvent } from "../event";
import { Examples } from "../examples";
import { memberTable } from "./member.sql";
import { memberTable, role } from "./member.sql";
import { and, eq, sql, asc, isNull } from "../drizzle";
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
@@ -17,7 +17,7 @@ export namespace Member {
description: Common.IdDescription,
example: Examples.Member.id,
}),
timeSeen: z.date().or(z.null()).openapi({
timeSeen: z.date().nullable().or(z.undefined()).openapi({
description: "The last time this team member was active",
example: Examples.Member.timeSeen
}),
@@ -25,6 +25,10 @@ export namespace Member {
description: "The unique id of the team this member is on",
example: Examples.Member.teamID
}),
role: z.enum(role).openapi({
description: "The role of this team member",
example: Examples.Member.role
}),
email: z.string().openapi({
description: "The email of this team member",
example: Examples.Member.email
@@ -68,6 +72,7 @@ export namespace Member {
id,
teamID: useTeam(),
email: input.email,
role: input.first ? "owner" : "member",
timeSeen: input.first ? sql`now()` : null,
})
@@ -113,11 +118,18 @@ export namespace Member {
),
)
/**
* Converts a raw member database row into a standardized {@link Member.Info} object.
*
* @param input - The database row representing a member.
* @returns The member information formatted as a {@link Member.Info} object.
*/
export function serialize(
input: typeof memberTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
role: input.role,
email: input.email,
teamID: input.teamID,
timeSeen: input.timeSeen

View File

@@ -1,12 +1,15 @@
import { teamIndexes } from "../team/team.sql";
import { timestamps, utc, teamID } from "../drizzle/types";
import { index, pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core";
import { index, pgTable, text, uniqueIndex, varchar } from "drizzle-orm/pg-core";
export const role = ["admin", "member", "owner"] as const;
export const memberTable = pgTable(
"member",
{
...teamID,
...timestamps,
role: text("role", { enum: role }).notNull(),
timeSeen: utc("time_seen"),
email: varchar("email", { length: 255 }).notNull(),
},

View File

@@ -1,69 +1,16 @@
import { z } from "zod";
import { fn } from "../utils";
import { Resource } from "sst";
import { eq, and } from "../drizzle";
import { useTeam } from "../actor";
import { createEvent } from "../event";
// import { polarTable, Standing } from "./polar.sql.ts.test";
import { useTeam, useUserID } from "../actor";
import { Polar as PolarSdk } from "@polar-sh/sdk";
import { useTransaction } from "../drizzle/transaction";
import { validateEvent } from "@polar-sh/sdk/webhooks";
import { PlanType } from "../subscription/subscription.sql";
const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" });
const planType = z.enum(PlanType)
export namespace Polar {
export const client = polar;
export const Info = z.object({
teamID: z.string(),
subscriptionID: z.string().nullable(),
customerID: z.string(),
subscriptionItemID: z.string().nullable(),
// standing: z.enum(Standing),
});
export type Info = z.infer<typeof Info>;
export const Checkout = z.object({
annual: z.boolean().optional(),
successUrl: z.string(),
cancelUrl: z.string(),
});
export const CheckoutSession = z.object({
url: z.string().nullable(),
});
export const CustomerSubscriptionEventType = [
"created",
"updated",
"deleted",
] as const;
export const Events = {
CustomerSubscriptionEvent: createEvent(
"polar.customer-subscription-event",
z.object({
type: z.enum(CustomerSubscriptionEventType),
status: z.string(),
teamID: z.string().min(1),
customerID: z.string().min(1),
subscriptionID: z.string().min(1),
subscriptionItemID: z.string().min(1),
}),
),
};
// export function get() {
// return useTransaction(async (tx) =>
// tx
// .select()
// .from(polarTable)
// .where(eq(polarTable.teamID, useTeam()))
// .execute()
// .then((rows) => rows.map(serialize).at(0)),
// );
// }
export const fromUserEmail = fn(z.string().min(1), async (email) => {
try {
const customers = await client.customers.list({ email })
@@ -81,89 +28,69 @@ export namespace Polar {
}
})
// export const setCustomerID = fn(Info.shape.customerID, async (customerID) =>
// useTransaction(async (tx) =>
// tx
// .insert(polarTable)
// .values({
// teamID: useTeam(),
// customerID,
// standing: "new",
// })
// .execute(),
// ),
// );
const getProductIDs = (plan: z.infer<typeof planType>) => {
switch (plan) {
case "free":
return [Resource.NestriFreeMonthly.value]
case "pro":
return [Resource.NestriProYearly.value, Resource.NestriProMonthly.value]
case "family":
return [Resource.NestriFamilyYearly.value, Resource.NestriFamilyMonthly.value]
default:
return [Resource.NestriFreeMonthly.value]
}
}
// export const setSubscription = fn(
// Info.pick({
// subscriptionID: true,
// subscriptionItemID: true,
// }),
// (input) =>
// useTransaction(async (tx) =>
// tx
// .update(polarTable)
// .set({
// subscriptionID: input.subscriptionID,
// subscriptionItemID: input.subscriptionItemID,
// })
// .where(eq(polarTable.teamID, useTeam()))
// .returning()
// .execute()
// .then((rows) => rows.map(serialize).at(0)),
// ),
// );
export const createPortal = fn(
z.string(),
async (customerId) => {
const session = await client.customerSessions.create({
customerId
})
// export const removeSubscription = fn(
// z.string().min(1),
// (stripeSubscriptionID) =>
// useTransaction((tx) =>
// tx
// .update(polarTable)
// .set({
// subscriptionItemID: null,
// subscriptionID: null,
// })
// .where(and(eq(polarTable.subscriptionID, stripeSubscriptionID)))
// .execute(),
// ),
// );
return session.customerPortalUrl
}
)
// export const setStanding = fn(
// Info.pick({
// subscriptionID: true,
// standing: true,
// }),
// (input) =>
// useTransaction((tx) =>
// tx
// .update(polarTable)
// .set({ standing: input.standing })
// .where(and(eq(polarTable.subscriptionID, input.subscriptionID!)))
// .execute(),
// ),
// );
//TODO: Implement this
export const handleWebhook = async(payload: ReturnType<typeof validateEvent>) => {
switch (payload.type) {
case "subscription.created":
const teamID = payload.data.metadata.teamID
}
}
// export const fromCustomerID = fn(Info.shape.customerID, (customerID) =>
// useTransaction((tx) =>
// tx
// .select()
// .from(polarTable)
// .where(and(eq(polarTable.customerID, customerID)))
// .execute()
// .then((rows) => rows.map(serialize).at(0)),
// ),
// );
export const createCheckout = fn(
z
.object({
planType: z.enum(PlanType),
customerEmail: z.string(),
successUrl: z.string(),
customerID: z.string(),
allowDiscountCodes: z.boolean(),
teamID: z.string()
})
.partial({
customerEmail: true,
allowDiscountCodes: true,
customerID: true,
teamID: true
}),
async (input) => {
const productIDs = getProductIDs(input.planType)
// function serialize(
// input: typeof polarTable.$inferSelect,
// ): z.infer<typeof Info> {
// return {
// teamID: input.teamID,
// customerID: input.customerID,
// subscriptionID: input.subscriptionID,
// subscriptionItemID: input.subscriptionItemID,
// standing: input.standing,
// };
// }
const checkoutUrl =
await client.checkouts.create({
products: productIDs,
customerEmail: input.customerEmail ?? useUserID(),
successUrl: `${input.successUrl}?checkout={CHECKOUT_ID}`,
allowDiscountCodes: input.allowDiscountCodes ?? false,
customerId: input.customerID,
customerMetadata: {
teamID: input.teamID ?? useTeam()
}
})
return checkoutUrl.url
})
}

View File

@@ -1,23 +0,0 @@
import { timestamps, teamID } from "../drizzle/types";
import { teamIndexes, teamTable } from "../team/team.sql";
import { pgTable, text, varchar } from "drizzle-orm/pg-core";
// FIXME: This is causing errors while trying to db push
export const Standing = ["new", "good", "overdue"] as const;
export const polarTable = pgTable(
"polar",
{
teamID: teamID.teamID.primaryKey().references(() => teamTable.id),
...timestamps,
customerID: varchar("customer_id", { length: 255 }).notNull(),
subscriptionID: varchar("subscription_id", { length: 255 }),
subscriptionItemID: varchar("subscription_item_id", {
length: 255,
}),
standing: text("standing", { enum: Standing }).notNull(),
},
(table) => [
...teamIndexes(table),
]
)

View File

@@ -1,24 +1,7 @@
import { z } from "zod";
import { id, timestamps, ulid, userID, utc } from "../drizzle/types";
import { index, pgTable, integer, uniqueIndex, varchar, text, primaryKey, json } from "drizzle-orm/pg-core";
import { userTable } from "../user/user.sql";
// public string Username { get; set; } = string.Empty;
// public ulong SteamId { get; set; }
// public string Email { get; set; } = string.Empty;
// public string Country { get; set; } = string.Empty;
// public string PersonaName { get; set; } = string.Empty;
// public string AvatarUrl { get; set; } = string.Empty;
// public bool IsLimited { get; set; }
// public bool IsLocked { get; set; }
// public bool IsBanned { get; set; }
// public bool IsAllowedToInviteFriends { get; set; }
// public ulong GameId { get; set; }
// public string GamePlayingName { get; set; } = string.Empty;
// public DateTime LastLogOn { get; set; }
// public DateTime LastLogOff { get; set; }
// public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
import { id, timestamps, ulid, utc } from "../drizzle/types";
import { index, pgTable, integer, uniqueIndex, varchar, text, json } from "drizzle-orm/pg-core";
export const LastGame = z.object({
gameID: z.number(),
@@ -54,5 +37,9 @@ export const steamTable = pgTable(
steamEmail: varchar("steam_email", { length: 255 }).notNull(),
personaName: varchar("persona_name", { length: 255 }).notNull(),
limitation: json("limitation").$type<AccountLimitation>().notNull(),
}
},
(table) => [
uniqueIndex("steam_id").on(table.steamID),
index("steam_user_id").on(table.userID),
],
);

View File

@@ -0,0 +1,192 @@
import { z } from "zod";
import { Common } from "../common";
import { Examples } from "../examples";
import { createID, fn } from "../utils";
import { eq, and, isNull } from "../drizzle";
import { useTeam, useUserID } from "../actor";
import { createTransaction, useTransaction } from "../drizzle/transaction";
import { PlanType, Standing, subscriptionTable } from "./subscription.sql";
export namespace Subscription {
export const Info = z.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Subscription.id,
}),
polarSubscriptionID: z.string().nullable().or(z.undefined()).openapi({
description: "The unique id of the plan this subscription is on",
example: Examples.Subscription.polarSubscriptionID,
}),
teamID: z.string().openapi({
description: "The unique id of the team this subscription is for",
example: Examples.Subscription.teamID,
}),
userID: z.string().openapi({
description: "The unique id of the user who is paying this subscription",
example: Examples.Subscription.userID,
}),
polarProductID: z.string().nullable().or(z.undefined()).openapi({
description: "The unique id of the product this subscription is for",
example: Examples.Subscription.polarProductID,
}),
tokens: z.number().openapi({
description: "The number of tokens this subscription has left",
example: Examples.Subscription.tokens,
}),
planType: z.enum(PlanType).openapi({
description: "The type of plan this subscription is for",
example: Examples.Subscription.planType,
}),
standing: z.enum(Standing).openapi({
description: "The standing of this subscription",
example: Examples.Subscription.standing,
}),
}).openapi({
ref: "Subscription",
description: "Represents a subscription on Nestri",
example: Examples.Subscription
});
export type Info = z.infer<typeof Info>;
export const create = fn(
Info
.partial({
teamID: true,
userID: true,
id: true,
standing: true,
planType: true,
polarProductID: true,
polarSubscriptionID: true,
}),
(input) =>
createTransaction(async (tx) => {
const id = input.id ?? createID("subscription");
await tx.insert(subscriptionTable).values({
id,
tokens: input.tokens,
polarProductID: input.polarProductID ?? null,
polarSubscriptionID: input.polarSubscriptionID ?? null,
standing: input.standing ?? "new",
planType: input.planType ?? "free",
userID: input.userID ?? useUserID(),
teamID: input.teamID ?? useTeam(),
});
return id;
})
)
export const setPolarProductID = fn(
Info.pick({
id: true,
polarProductID: true,
}),
(input) =>
useTransaction(async (tx) =>
tx.update(subscriptionTable)
.set({
polarProductID: input.polarProductID,
})
.where(eq(subscriptionTable.id, input.id))
)
)
export const setPolarSubscriptionID = fn(
Info.pick({
id: true,
polarSubscriptionID: true,
}),
(input) =>
useTransaction(async (tx) =>
tx.update(subscriptionTable)
.set({
polarSubscriptionID: input.polarSubscriptionID,
})
.where(eq(subscriptionTable.id, input.id))
)
)
export const fromID = fn(z.string(), async (id) =>
useTransaction(async (tx) =>
tx
.select()
.from(subscriptionTable)
.where(
and(
eq(subscriptionTable.id, id),
isNull(subscriptionTable.timeDeleted)
)
)
.orderBy(subscriptionTable.timeCreated)
.then((rows) => rows.map(serialize))
)
)
export const fromTeamID = fn(z.string(), async (teamID) =>
useTransaction(async (tx) =>
tx
.select()
.from(subscriptionTable)
.where(
and(
eq(subscriptionTable.teamID, teamID),
isNull(subscriptionTable.timeDeleted)
)
)
.orderBy(subscriptionTable.timeCreated)
.then((rows) => rows.map(serialize))
)
)
export const fromUserID = fn(z.string(), async (userID) =>
useTransaction(async (tx) =>
tx
.select()
.from(subscriptionTable)
.where(
and(
eq(subscriptionTable.userID, userID),
isNull(subscriptionTable.timeDeleted)
)
)
.orderBy(subscriptionTable.timeCreated)
.then((rows) => rows.map(serialize))
)
)
export const remove = fn(Info.shape.id, (id) =>
useTransaction(async (tx) =>
tx
.update(subscriptionTable)
.set({
timeDeleted: Common.now(),
})
.where(eq(subscriptionTable.id, id))
.execute()
)
)
/**
* Converts a raw subscription database record into a structured {@link Info} object.
*
* @param input - The subscription record retrieved from the database.
* @returns The subscription data formatted according to the {@link Info} schema.
*/
export function serialize(
input: typeof subscriptionTable.$inferSelect
): z.infer<typeof Info> {
return {
id: input.id,
userID: input.userID,
teamID: input.teamID,
standing: input.standing,
planType: input.planType,
tokens: input.tokens,
polarProductID: input.polarProductID,
polarSubscriptionID: input.polarSubscriptionID,
};
}
}

View File

@@ -0,0 +1,31 @@
import { teamTable } from "../team/team.sql";
import { ulid, userID, timestamps } from "../drizzle/types";
import { index, integer, pgTable, primaryKey, text, uniqueIndex, varchar } from "drizzle-orm/pg-core";
export const Standing = ["new", "good", "overdue", "cancelled"] as const;
export const PlanType = ["free", "pro", "family", "enterprise"] as const;
export const subscriptionTable = pgTable(
"subscription",
{
...userID,
...timestamps,
teamID: ulid("team_id")
.references(() => teamTable.id, { onDelete: "cascade" })
.notNull(),
standing: text("standing", { enum: Standing })
.notNull(),
planType: text("plan_type", { enum: PlanType })
.notNull(),
tokens: integer("tokens").notNull(),
polarProductID: varchar("product_id", { length: 255 }),
polarSubscriptionID: varchar("subscription_id", { length: 255 }),
},
(table) => [
uniqueIndex("subscription_id").on(table.id),
index("subscription_user_id").on(table.userID),
primaryKey({
columns: [table.id, table.teamID]
}),
]
)

View File

@@ -1,16 +1,18 @@
import { z } from "zod";
import { Resource } from "sst";
import { bus } from "sst/aws/bus";
import { Common } from "../common";
import { Member } from "../member";
import { teamTable } from "./team.sql";
import { Examples } from "../examples";
import { assertActor } from "../actor";
import { createEvent } from "../event";
import { createID, fn } from "../utils";
import { Subscription } from "../subscription";
import { and, eq, sql, isNull } from "../drizzle";
import { PlanType, teamTable } from "./team.sql";
import { assertActor, withActor } from "../actor";
import { memberTable } from "../member/member.sql";
import { ErrorCodes, VisibleError } from "../error";
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
import { groupBy, map, pipe, values } from "remeda";
import { subscriptionTable } from "../subscription/subscription.sql";
import { createTransaction, useTransaction } from "../drizzle/transaction";
export namespace Team {
export const Info = z
@@ -19,6 +21,7 @@ export namespace Team {
description: Common.IdDescription,
example: Examples.Team.id,
}),
// Remove spaces and make sure it is lowercase (this is just to make sure the frontend did this)
slug: z.string().regex(/^[a-z0-9\-]+$/, "Use a URL friendly name.").openapi({
description: "The unique and url-friendly slug of this team",
example: Examples.Team.slug
@@ -27,10 +30,14 @@ export namespace Team {
description: "The name of this team",
example: Examples.Team.name
}),
planType: z.enum(PlanType).openapi({
description: "The type of Plan this team is subscribed to",
example: Examples.Team.planType
})
members: Member.Info.array().openapi({
description: "The members of this team",
example: Examples.Team.members
}),
subscriptions: Subscription.Info.array().openapi({
description: "The subscriptions of this team",
example: Examples.Team.subscriptions
}),
})
.openapi({
ref: "Team",
@@ -60,16 +67,14 @@ export namespace Team {
}
export const create = fn(
Info.pick({ slug: true, id: true, name: true, planType: true }).partial({
Info.pick({ slug: true, id: true, name: true, }).partial({
id: true,
}), (input) =>
createTransaction(async (tx) => {
const id = input.id ?? createID("team");
const result = await tx.insert(teamTable).values({
id,
//Remove spaces and make sure it is lowercase (this is just to make sure the frontend did this)
slug: input.slug, //.toLowerCase().replace(/[\s]/g, ''),
planType: input.planType,
slug: input.slug,
name: input.name
})
.onConflictDoNothing({ target: teamTable.slug })
@@ -80,6 +85,7 @@ export namespace Team {
})
)
//TODO: "Delete" subscription and member(s) as well
export const remove = fn(Info.shape.id, (input) =>
useTransaction(async (tx) => {
const account = assertActor("user");
@@ -106,48 +112,107 @@ export namespace Team {
}),
);
export const list = fn(z.void(), () =>
useTransaction((tx) =>
export const list = fn(z.void(), () => {
const actor = assertActor("user");
return useTransaction(async (tx) =>
tx
.select()
.from(teamTable)
.where(isNull(teamTable.timeDeleted))
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
.where(
and(
eq(memberTable.email, actor.properties.email),
isNull(memberTable.timeDeleted),
isNull(teamTable.timeDeleted),
),
)
.execute()
.then((rows) => rows.map(serialize)),
.then((rows) => serialize(rows))
)
});
export const fromID = fn(z.string().min(1), async (id) =>
useTransaction(async (tx) =>
tx
.select()
.from(teamTable)
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
.where(
and(
eq(teamTable.id, id),
isNull(memberTable.timeDeleted),
isNull(teamTable.timeDeleted),
),
)
.execute()
.then((rows) => serialize(rows).at(0))
),
);
export const fromID = fn(z.string().min(1), async (id) =>
useTransaction(async (tx) => {
return tx
export const fromSlug = fn(z.string().min(1), async (slug) =>
useTransaction(async (tx) =>
tx
.select()
.from(teamTable)
.where(and(eq(teamTable.id, id), isNull(teamTable.timeDeleted)))
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
.where(
and(
eq(teamTable.slug, slug),
isNull(memberTable.timeDeleted),
isNull(teamTable.timeDeleted),
),
)
.execute()
.then((rows) => rows.map(serialize).at(0))
}),
);
export const fromSlug = fn(z.string().min(1), async (input) =>
useTransaction(async (tx) => {
return tx
.select()
.from(teamTable)
.where(and(eq(teamTable.slug, input), isNull(teamTable.timeDeleted)))
.execute()
.then((rows) => rows.map(serialize).at(0))
}),
.then((rows) => serialize(rows).at(0))
),
);
/**
* Transforms an array of team, subscription, and member records into structured team objects.
*
* Groups input rows by team ID and constructs an array of team objects, each including its associated members and subscriptions.
*
* @param input - Array of objects containing team, subscription, and member data.
* @returns An array of team objects with their members and subscriptions.
*/
export function serialize(
input: typeof teamTable.$inferSelect,
): z.infer<typeof Info> {
return {
id: input.id,
name: input.name,
slug: input.slug,
planType: input.planType,
};
input: { team: typeof teamTable.$inferSelect, subscription: typeof subscriptionTable.$inferInsert | null, member: typeof memberTable.$inferInsert | null }[],
): z.infer<typeof Info>[] {
console.log("serialize", input)
return pipe(
input,
groupBy((row) => row.team.id),
values(),
map((group) => ({
name: group[0].team.name,
id: group[0].team.id,
slug: group[0].team.slug,
subscriptions: !group[0].subscription ?
[] :
group.map((row) => ({
planType: row.subscription!.planType,
polarProductID: row.subscription!.polarProductID,
polarSubscriptionID: row.subscription!.polarSubscriptionID,
standing: row.subscription!.standing,
tokens: row.subscription!.tokens,
teamID: row.subscription!.teamID,
userID: row.subscription!.userID,
id: row.subscription!.id,
})),
members:
!group[0].member ?
[] :
group.map((row) => ({
id: row.member!.id,
email: row.member!.email,
role: row.member!.role,
teamID: row.member!.teamID,
timeSeen: row.member!.timeSeen,
}))
})),
);
}
}

View File

@@ -1,15 +1,11 @@
import { } from "drizzle-orm/postgres-js";
import { timestamps, id } from "../drizzle/types";
import {
varchar,
pgTable,
primaryKey,
uniqueIndex,
text
} from "drizzle-orm/pg-core";
export const PlanType = ["Hosted", "BYOG"] as const;
export const teamTable = pgTable(
"team",
{
@@ -17,7 +13,6 @@ export const teamTable = pgTable(
...timestamps,
name: varchar("name", { length: 255 }).notNull(),
slug: varchar("slug", { length: 255 }).notNull(),
planType: text("plan_type", { enum: PlanType }).notNull()
},
(table) => [
uniqueIndex("slug").on(table.slug)

View File

@@ -1,21 +1,22 @@
import { z } from "zod";
import { Team } from "../team";
import { bus } from "sst/aws/bus";
import { Steam } from "../steam";
import { Common } from "../common";
import { Polar } from "../polar/index";
import { createID, fn } from "../utils";
import { userTable } from "./user.sql";
import { createEvent } from "../event";
import { pipe, groupBy, values, map } from "remeda";
import { Examples } from "../examples";
import { Resource } from "sst/resource";
import { teamTable } from "../team/team.sql";
import { steamTable } from "../steam/steam.sql";
import { assertActor, withActor } from "../actor";
import { memberTable } from "../member/member.sql";
import { and, eq, isNull, asc, getTableColumns, sql } from "../drizzle";
import { pipe, groupBy, values, map } from "remeda";
import { and, eq, isNull, asc, sql } from "../drizzle";
import { subscriptionTable } from "../subscription/subscription.sql";
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
import { Steam } from "../steam";
export namespace User {
@@ -154,91 +155,27 @@ export namespace User {
})
export const fromEmail = fn(z.string(), async (email) =>
useTransaction(async (tx) => {
const rows = await tx
useTransaction(async (tx) =>
tx
.select()
.from(userTable)
.leftJoin(steamTable, eq(userTable.id, steamTable.userID))
.where(and(eq(userTable.email, email), isNull(userTable.timeDeleted)))
.orderBy(asc(userTable.timeCreated))
const result = pipe(
rows,
groupBy((row) => row.user.id),
values(),
map(
(group): Info => ({
id: group[0].user.id,
name: group[0].user.name,
email: group[0].user.email,
avatarUrl: group[0].user.avatarUrl,
discriminator: group[0].user.discriminator,
polarCustomerID: group[0].user.polarCustomerID,
steamAccounts: !group[0].steam ?
[] :
group.map((row) => ({
id: row.steam!.id,
userID: row.steam!.userID,
steamID: row.steam!.steamID,
lastSeen: row.steam!.lastSeen,
avatarUrl: row.steam!.avatarUrl,
lastGame: row.steam!.lastGame,
username: row.steam!.username,
countryCode: row.steam!.countryCode,
steamEmail: row.steam!.steamEmail,
personaName: row.steam!.personaName,
limitation: row.steam!.limitation,
})),
})
)
)
return result[0]
}),
.then((rows => serialize(rows).at(0)))
)
)
export const fromID = fn(z.string(), async (id) =>
useTransaction(async (tx) => {
const rows = await tx
export const fromID = fn(z.string(), (id) =>
useTransaction(async (tx) =>
tx
.select()
.from(userTable)
.leftJoin(steamTable, eq(userTable.id, steamTable.userID))
.where(and(eq(userTable.id, id), isNull(userTable.timeDeleted)))
.where(and(eq(userTable.id, id), isNull(userTable.timeDeleted), isNull(steamTable.timeDeleted)))
.orderBy(asc(userTable.timeCreated))
const result = pipe(
rows,
groupBy((row) => row.user.id),
values(),
map(
(group): Info => ({
id: group[0].user.id,
name: group[0].user.name,
email: group[0].user.email,
avatarUrl: group[0].user.avatarUrl,
discriminator: group[0].user.discriminator,
polarCustomerID: group[0].user.polarCustomerID,
steamAccounts: !group[0].steam ?
[] :
group.map((row) => ({
id: row.steam!.id,
userID: row.steam!.userID,
steamID: row.steam!.steamID,
lastSeen: row.steam!.lastSeen,
avatarUrl: row.steam!.avatarUrl,
lastGame: row.steam!.lastGame,
username: row.steam!.username,
countryCode: row.steam!.countryCode,
steamEmail: row.steam!.steamEmail,
personaName: row.steam!.personaName,
limitation: row.steam!.limitation,
})),
})
)
)
return result[0]
}),
.then((rows) => serialize(rows).at(0))
),
)
export const remove = fn(Info.shape.id, (id) =>
@@ -254,12 +191,54 @@ export namespace User {
}),
);
/**
* Converts an array of user and Steam account records into structured user objects with associated Steam accounts.
*
* @param input - An array of objects containing user data and optional Steam account data.
* @returns An array of user objects, each including a list of their associated Steam accounts.
*/
export function serialize(
input: { user: typeof userTable.$inferSelect; steam: typeof steamTable.$inferSelect | null }[],
): z.infer<typeof Info>[] {
return pipe(
input,
groupBy((row) => row.user.id),
values(),
map((group) => ({
...group[0].user,
steamAccounts: !group[0].steam ?
[] :
group.map((row) => ({
id: row.steam!.id,
lastSeen: row.steam!.lastSeen,
countryCode: row.steam!.countryCode,
username: row.steam!.username,
steamID: row.steam!.steamID,
lastGame: row.steam!.lastGame,
limitation: row.steam!.limitation,
steamEmail: row.steam!.steamEmail,
userID: row.steam!.userID,
personaName: row.steam!.personaName,
avatarUrl: row.steam!.avatarUrl,
})),
})),
)
}
/**
* Retrieves the list of teams that the current user belongs to.
*
* @returns An array of team information objects representing the user's active team memberships.
*
* @remark Only teams and memberships that have not been deleted are included in the result.
*/
export function teams() {
const actor = assertActor("user");
return useTransaction((tx) =>
return useTransaction(async (tx) =>
tx
.select(getTableColumns(teamTable))
.select()
.from(teamTable)
.leftJoin(subscriptionTable, eq(subscriptionTable.teamID, teamTable.id))
.innerJoin(memberTable, eq(memberTable.teamID, teamTable.id))
.where(
and(
@@ -269,7 +248,7 @@ export namespace User {
),
)
.execute()
.then((rows) => rows.map(Team.serialize))
);
.then((rows) => Team.serialize(rows))
)
}
}

View File

@@ -2,11 +2,14 @@ import { ulid } from "ulid";
export const prefixes = {
user: "usr",
team: "tea",
team: "tem",
task: "tsk",
machine: "mch",
member: "mbr",
steam: "stm",
subscription: "sub",
invite: "inv",
product: "prd",
} as const;
/**

View File

@@ -42,11 +42,6 @@ export const auth: MiddlewareHandler = async (c, next) => {
"Invalid bearer token",
);
}
if (result.subject.type === "machine") {
console.log("machine detected")
return withActor(result.subject, next);
}
if (result.subject.type === "user") {
const teamID = c.req.header("x-nestri-team");
@@ -58,12 +53,11 @@ export const auth: MiddlewareHandler = async (c, next) => {
teamID,
},
},
async () => {
return withActor(
async () =>
withActor(
result.subject,
next,
);
},
)
);
}
};

View File

@@ -3,7 +3,7 @@ import { Hono } from "hono";
import { auth } from "./auth";
import { cors } from "hono/cors";
import { TeamApi } from "./team";
import { SteamApi } from "./steam";
import { PolarApi } from "./polar";
import { logger } from "hono/logger";
import { Realtime } from "./realtime";
import { AccountApi } from "./account";
@@ -27,7 +27,7 @@ const routes = app
.get("/", (c) => c.text("Hello World!"))
.route("/realtime", Realtime.route)
.route("/team", TeamApi.route)
.route("/steam", SteamApi.route)
.route("/polar", PolarApi.route)
.route("/account", AccountApi.route)
.route("/machine", MachineApi.route)
.onError((error, c) => {

View File

@@ -0,0 +1,174 @@
import { z } from "zod";
import { Hono } from "hono";
import { Resource } from "sst";
import { notPublic } from "./auth";
import { describeRoute } from "hono-openapi";
import { User } from "@nestri/core/user/index";
import { assertActor } from "@nestri/core/actor";
import { Polar } from "@nestri/core/polar/index";
import { Examples } from "@nestri/core/examples";
import { ErrorResponses, Result, validator } from "./common";
import { ErrorCodes, VisibleError } from "@nestri/core/error";
import { PlanType } from "@nestri/core/subscription/subscription.sql";
import { WebhookVerificationError, validateEvent } from "@polar-sh/sdk/webhooks";
export namespace PolarApi {
export const route = new Hono()
.use(notPublic)
.get("/",
describeRoute({
tags: ["Polar"],
summary: "Create a Polar.sh customer portal",
description: "Creates Polar.sh's customer portal url where the user can manage their payments",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
portalUrl: z.string()
}).openapi({
description: "The customer portal url",
example: { portalUrl: "https://polar.sh/portal/39393jdie09292" }
})
),
},
},
description: "customer portal url"
},
400: ErrorResponses[400],
404: ErrorResponses[404],
429: ErrorResponses[429],
}
}),
async (c) => {
const actor = assertActor("user");
const user = await User.fromID(actor.properties.userID);
if (!user)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User not found",
);
if (!user.polarCustomerID)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User does not contain Polar customer ID"
)
const portalUrl = await Polar.createPortal(user.polarCustomerID)
return c.json({
data: {
portalUrl
}
})
}
)
.post("/checkout",
describeRoute({
tags: ["Polar"],
summary: "Create a checkout url",
description: "Creates a Polar.sh's checkout url for the user to pay a subscription for this team",
responses: {
200: {
content: {
"application/json": {
schema: Result(
z.object({
checkoutUrl: z.string()
}).openapi({
description: "The checkout url",
example: { checkoutUrl: "https://polar.sh/portal/39393jdie09292" }
})
),
},
},
description: "checkout url"
},
400: ErrorResponses[400],
404: ErrorResponses[404],
429: ErrorResponses[429],
}
}),
validator(
"json",
z
.object({
planType: z.enum(PlanType),
successUrl: z.string().url("Success url must be a valid url")
})
.openapi({
description: "Details of the team to create",
example: {
planType: Examples.Subscription.planType,
successUrl: "https://your-url.io/thanks"
},
})
),
async (c) => {
const body = c.req.valid("json");
const actor = assertActor("user");
const user = await User.fromID(actor.properties.userID);
if (!user)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User not found",
);
if (!user.polarCustomerID)
throw new VisibleError(
"not_found",
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
"User does not contain Polar customer ID"
)
const checkoutUrl = await Polar.createCheckout({ customerID: user.polarCustomerID, planType: body.planType, successUrl: body.successUrl })
return c.json({
data: {
checkoutUrl,
}
})
}
)
.post("/webhook",
async (c) => {
const requestBody = await c.req.text();
const webhookSecret = Resource.PolarWebhookSecret.value
const webhookHeaders = {
"webhook-id": c.req.header("webhook-id") ?? "",
"webhook-timestamp": c.req.header("webhook-timestamp") ?? "",
"webhook-signature": c.req.header("webhook-signature") ?? "",
};
let webhookPayload: ReturnType<typeof validateEvent>;
try {
webhookPayload = validateEvent(
requestBody,
webhookHeaders,
webhookSecret,
);
} catch (error) {
if (error instanceof WebhookVerificationError) {
return c.json({ received: false }, { status: 403 });
}
throw error;
}
await Polar.handleWebhook(webhookPayload)
return c.json({ received: true });
}
)
}

View File

@@ -0,0 +1,46 @@
import { Hono } from "hono";
import { notPublic } from "./auth";
import { describeRoute } from "hono-openapi";
import { Examples } from "@nestri/core/examples";
import { assertActor } from "@nestri/core/actor";
import { ErrorResponses, Result } from "./common";
import { Subscription } from "@nestri/core/subscription/index";
export namespace SubscriptionApi {
export const route = new Hono()
.use(notPublic)
.get("/",
describeRoute({
tags: ["Subscription"],
summary: "Get user subscriptions",
description: "Get all user subscriptions",
responses: {
200: {
content: {
"application/json": {
schema: Result(
Subscription.Info.array().openapi({
description: "All the subscriptions this user has",
example: [Examples.Subscription]
})
),
},
},
description: "All user subscriptions"
},
400: ErrorResponses[400],
404: ErrorResponses[404],
429: ErrorResponses[429],
}
}),
async (c) => {
const actor = assertActor("user")
const subscriptions = await Subscription.fromUserID(actor.properties.userID)
return c.json({
data: subscriptions
})
}
)
}

View File

@@ -5,9 +5,12 @@ import { describeRoute } from "hono-openapi";
import { User } from "@nestri/core/user/index";
import { Team } from "@nestri/core/team/index";
import { Examples } from "@nestri/core/examples";
import { Polar } from "@nestri/core/polar/index";
import { Member } from "@nestri/core/member/index";
import { assertActor, withActor } from "@nestri/core/actor";
import { ErrorResponses, Result, validator } from "./common";
import { Subscription } from "@nestri/core/subscription/index";
import { PlanType } from "@nestri/core/subscription/subscription.sql";
export namespace TeamApi {
export const route = new Hono()
@@ -49,7 +52,12 @@ export namespace TeamApi {
content: {
"application/json": {
schema: Result(
z.literal("ok")
z.object({
checkoutUrl: z.string().openapi({
description: "The checkout url to confirm subscription for this team",
example: "https://polar.sh/checkout/2903038439320298377"
})
})
)
}
},
@@ -63,17 +71,24 @@ export namespace TeamApi {
}),
validator(
"json",
Team.create.schema.omit({ id: true }).openapi({
description: "Details of the team to create",
//@ts-expect-error
example: { ...Examples.Team, id: undefined }
})
Team.create.schema
.pick({ slug: true, name: true })
.extend({ planType: z.enum(PlanType), successUrl: z.string().url("Success url must be a valid url") })
.openapi({
description: "Details of the team to create",
example: {
slug: Examples.Team.slug,
name: Examples.Team.name,
planType: Examples.Subscription.planType,
successUrl: "https://your-url.io/thanks"
},
})
),
async (c) => {
const body = c.req.valid("json")
const actor = assertActor("user");
const teamID = await Team.create(body);
const teamID = await Team.create({ name: body.name, slug: body.slug });
await withActor(
{
@@ -82,14 +97,28 @@ export namespace TeamApi {
teamID,
},
},
() =>
Member.create({
async () => {
await Member.create({
first: true,
email: actor.properties.email,
}),
});
await Subscription.create({
planType: body.planType,
userID: actor.properties.userID,
// FIXME: Make this make sense
tokens: body.planType === "free" ? 100 : body.planType === "pro" ? 1000 : body.planType === "family" ? 10000 : 0,
});
}
);
return c.json({ data: "ok" })
const checkoutUrl = await Polar.createCheckout({ planType: body.planType, successUrl: body.successUrl, teamID })
return c.json({
data: {
checkoutUrl,
}
})
}
)
}

View File

@@ -57,10 +57,34 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"NestriFamilyMonthly": {
"type": "sst.sst.Secret"
"value": string
}
"NestriFamilyYearly": {
"type": "sst.sst.Secret"
"value": string
}
"NestriFreeMonthly": {
"type": "sst.sst.Secret"
"value": string
}
"NestriProMonthly": {
"type": "sst.sst.Secret"
"value": string
}
"NestriProYearly": {
"type": "sst.sst.Secret"
"value": string
}
"PolarSecret": {
"type": "sst.sst.Secret"
"value": string
}
"PolarWebhookSecret": {
"type": "sst.sst.Secret"
"value": string
}
"Realtime": {
"authorizer": string
"endpoint": string

0
packages/scripts/src/db-reset.sh Executable file → Normal file
View File

0
packages/scripts/src/psql.sh Executable file → Normal file
View File

View File

@@ -8,8 +8,8 @@ import { useNavigate } from "@solidjs/router";
import { useOpenAuth } from "@openauthjs/solid";
import { utility } from "@nestri/www/ui/utility";
import { useAccount } from "../providers/account";
import { Container, Screen as FullScreen } from "@nestri/www/ui/layout";
import { FormField, Input, Select } from "@nestri/www/ui/form";
import { Container, Screen as FullScreen } from "@nestri/www/ui/layout";
import { createForm, getValue, setError, valiForm } from "@modular-forms/solid";
const nameRegex = /^[a-z0-9\-]+$/
@@ -32,8 +32,9 @@ const Hr = styled("hr", {
})
const Plan = {
Pro: 'BYOG',
Basic: 'Hosted',
Free: 'free',
Pro: 'pro',
Family: 'family',
} as const;
const schema = v.object({
@@ -110,6 +111,13 @@ const UrlTitle = styled("span", {
}
})
/**
* Renders a form for creating a new team with validated fields for team name, slug, and plan type.
*
* Submits the form data to the API to create the team, displays validation errors, and navigates to the new team's page upon success.
*
* @remark If the chosen team slug is already taken, an error message is shown for the slug field.
*/
export function CreateTeamComponent() {
const [form, { Form, Field }] = createForm({
validate: valiForm(schema),
@@ -215,12 +223,14 @@ export function CreateTeamComponent() {
required
value={field.value}
badges={[
{ label: "BYOG", color: "purple" },
{ label: "Hosted", color: "blue" },
{ label: "Free", color: "gray" },
{ label: "Pro", color: "blue" },
{ label: "Family", color: "purple" },
]}
options={[
{ label: "I'll be playing on my machine", value: 'BYOG' },
{ label: "I'll be playing on the cloud", value: 'Hosted' },
{ label: "I'll be playing by myself", value: 'free' },
{ label: "I'll be playing with 3 friends", value: 'pro' },
{ label: "I'll be playing with 5 family members", value: 'family' },
]}
/>
</FormField>

View File

@@ -201,15 +201,12 @@ const Nav = styled("nav", {
})
/**
* Renders the application's header, featuring navigation, branding, and team details.
* Displays the application's fixed top navigation bar with branding, team information, and navigation links.
*
* This component displays a navigation bar that includes the logo, team avatar, team name, a badge
* reflecting the team's plan type, and navigation links. It adjusts its styling based on the scroll
* position by toggling visual effects on the navigation wrapper. A scroll event listener is added
* on mount to update the header's appearance when the user scrolls and is removed on unmount.
* The header includes the app logo, team avatar and name, a badge indicating the team's plan type, and navigation links related to the team. The header's appearance updates dynamically based on the user's scroll position.
*
* @param props.children - Optional child elements rendered below the header component.
* @returns The header component element.
* @param props.children - Optional elements rendered below the header.
* @returns The rendered header component.
*/
export function Header(props: ParentProps) {
// const team = useContext(TeamContext)
@@ -218,7 +215,7 @@ export function Header(props: ParentProps) {
id: "tea_01JPACSPYWTTJ66F32X3AWWFWE",
slug: "wanjohiryan",
name: "Wanjohi",
planType: "BYOG"
planType: "Pro"
})
createEffect(() => {
@@ -231,8 +228,9 @@ export function Header(props: ParentProps) {
});
})
// const account = useAccount()
return (
<PageWrapper>
<NavWrapper scrolled={hasScrolled()}>
@@ -294,14 +292,14 @@ export function Header(props: ParentProps) {
/>
<TeamLabel style={{ color: theme.color.d1000.gray }}>{team!().name}</TeamLabel>
<Switch>
<Match when={team!().planType === "BYOG"}>
<Match when={team!().planType === "Family"}>
<Badge style={{ "background-color": theme.color.purple.d700 }}>
<span style={{ "line-height": 0 }} >BYOG</span>
<span style={{ "line-height": 0 }} >Family</span>
</Badge>
</Match>
<Match when={team!().planType === "Hosted"}>
<Match when={team!().planType === "Pro"}>
<Badge style={{ "background-color": theme.color.blue.d700 }}>
<span style={{ "line-height": 0 }}>Hosted</span>
<span style={{ "line-height": 0 }}>Pro</span>
</Badge>
</Match>
</Switch>

View File

@@ -44,7 +44,7 @@ export const TeamRoute = (
return (
<Switch>
<Match when={!team()}>
TODO: Add a public page for (other) teams
{/* TODO: Add a public page for (other) teams */}
<NotAllowed header />
</Match>
<Match when={team()}>

View File

@@ -157,6 +157,7 @@ export const InputRadio = styled("input", {
const Label = styled("p", {
base: {
fontWeight: 500,
textAlign: "left",
letterSpacing: -0.1,
fontSize: theme.font.size.mono_sm,
textTransform: "capitalize",
@@ -213,6 +214,7 @@ const Hint = styled("p", {
fontSize: theme.font.size.xs,
lineHeight: theme.font.lineHeight,
color: theme.color.gray.d800,
textAlign: "left"
},
variants: {
color: {

24
sst-env.d.ts vendored
View File

@@ -57,10 +57,34 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"NestriFamilyMonthly": {
"type": "sst.sst.Secret"
"value": string
}
"NestriFamilyYearly": {
"type": "sst.sst.Secret"
"value": string
}
"NestriFreeMonthly": {
"type": "sst.sst.Secret"
"value": string
}
"NestriProMonthly": {
"type": "sst.sst.Secret"
"value": string
}
"NestriProYearly": {
"type": "sst.sst.Secret"
"value": string
}
"PolarSecret": {
"type": "sst.sst.Secret"
"value": string
}
"PolarWebhookSecret": {
"type": "sst.sst.Secret"
"value": string
}
"Realtime": {
"authorizer": string
"endpoint": string