stack/apps/dev-launchpad/public/index.html
Aman Ganapathy 1de8a17183
Payments bulldozer txn rework (#1315)
### Object of this PR
This PR is NOT a monolithic series of fixes for the payments suite + a
complete rework. Its aims were
a) introducing and robustly testing the bulldozer db system 
b) reworking the payments underlying architecture to use bulldozer for
correctness and scalability
c) Achieving parity with the old payments system excepting a few changes
like ensuring correctness of the ledger algo
There may still be some work to do with handling refunds, decoupling the
concepts of purchases from that of products, and some other things.

### Ledger Algorithm
This has been tuned and fixed. Item removals i.e negative item quantity
changes will apply to the soonest expiring item grant i.e positive item
quantity change. This is what is best for the user. Item grants can also
expire, and when they expire we obviate whatever is left of their
original capacity (meaning after all the removals that were applied to
it). Our ledger algo is applied via Bulldozer, so automatic
re-computation is handled when a new grant/ removal is inserted in the
middle of the existing ones.

### Things we got rid of 
* No more automatic support for default products. You can use $0 plan
provisions to accomplish the same effect but it's manual
* Negative item quantity changes (i.e item removals) no longer can have
expiries



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

* **New Features**
* Enhanced payment processing pipeline with improved data consistency
and state management.
  * Advanced refund handling with comprehensive transaction tracking.
* Better tracking and management of customer item quantities and owned
products.
* Improved subscription lifecycle management including period-end
handling.

* **Bug Fixes**
  * Fixed payment data integrity verification.
  * Improved handling of edge cases in refund scenarios.

* **Chores**
  * Updated cSpell configuration with additional words.
  * Expanded developer documentation for linting workflows.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Konstantin Wohlwend <n2d4xc@gmail.com>
Co-authored-by: Aadesh Kheria <kheriaaadesh@gmail.com>
Co-authored-by: Mantra <87142457+mantrakp04@users.noreply.github.com>
2026-04-17 22:11:21 +00:00

420 lines
14 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Stack Auth Dev Launchpad</title>
<script src="./env-config.js"></script>
<style>
body {
font-family: Arial, sans-serif;
background-color: #e0f0e0;
padding-left: 16px;
padding-right: 16px;
}
.apps-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 16px;
}
.apps-container > a {
border: 1px solid #8888;
background-color: #fff;
padding: 0px 4px 8px 4px;
width: 120px;
text-decoration: none;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
position: relative;
}
.apps-container > a.important {
background-color: #fee;
}
.apps-container > a.unimportant {
opacity: 0.2;
}
.apps-container > a.unimportant:hover {
opacity: 0.5;
}
.apps-container > a:hover {
border-color: #888;
transition: opacity 0.1s ease-in-out;
}
.apps-container > a > div > img {
height: 68px;
}
.apps-container > a > .description {
text-align: center;
font-size: 12px;
color: #888;
}
.apps-container > a > .port {
padding-right: 0.5px;
padding-top: 1px;
align-self: flex-end;
font-size: 12px;
color: #888;
}
.apps-container > a > .hover-description {
display: none;
position: absolute;
top: 100%;
left: -1px;
pointer-events: none;
z-index: 1000;
white-space: pre;
background-color: #ffc;
border: 1px solid #888;
padding: 2px;
color: #0008;
font-size: 12px;
}
.apps-container > a:hover > .hover-description {
display: block;
}
</style>
</head>
<body>
<h1>Stack Auth Dev Launchpad</h1>
<div class="apps-container"></div>
<hr />
<div class="apps-container"></div>
<hr />
<div class="apps-container"></div>
<h2 style="margin-top: 64px;">Background services</h2>
<ul class="background-services"></ul>
<noscript>
This page requires JavaScript.
</noscript>
<script>
const derivePrefixFromLocation = () => {
const port = window.location.port;
if (!port || port.length < 2) return "81";
return port.slice(0, -2);
};
const stackPortPrefix = window.NEXT_PUBLIC_STACK_PORT_PREFIX || derivePrefixFromLocation();
window.NEXT_PUBLIC_STACK_PORT_PREFIX = stackPortPrefix;
const withPrefix = (suffix) => `${stackPortPrefix}${suffix}`;
// Depending on the port prefix, set the color to light grey (port 91), light purple (port 92), papyrus yellow (port 93), or default otherwise
const color = {
"91": "#f8f8f8",
"92": "#fff8e0",
"93": "#e0e0ff",
}[stackPortPrefix] || undefined;
document.body.style.backgroundColor = color;
const backgroundServices = [
{ suffix: "28", label: "PostgreSQL" },
{ suffix: "34", label: "PostgreSQL Replica (with replication lag)" },
{ suffix: "29", label: "Inbucket SMTP" },
{ suffix: "30", label: "Inbucket POP3" },
{ suffix: "31", label: "OTel collector" },
{ suffix: "21", label: "S3 mock" },
{ suffix: "22", label: "Freestyle mock" },
{ suffix: "24", label: "LocalStack Gateway (AWS mock)" },
{ suffix: "25", label: "QStash mock" },
{ suffix: "33", label: "ClickHouse native interface" },
{ suffix: "39", label: "SpacetimeDB (MCP call log)" },
{ range: ["50", "99"], label: "Reserved for LocalStack (external services)" },
];
const backgroundList = document.querySelector(".background-services");
backgroundList.innerHTML = backgroundServices.map((service) => {
const portText = service.range
? `${withPrefix(service.range[0])}-${withPrefix(service.range[1])}`
: withPrefix(service.suffix);
return `<li>${portText}: ${service.label}</li>`;
}).join("");
const apps = [
{
name: "Dashboard",
portSuffix: "01",
description: [
"Src: ./apps/dashboard",
"Prod: https://app.stack-auth.com",
],
img: "https://www.svgrepo.com/show/507260/dashboard.svg",
importance: 2,
},
{
name: "Backend",
portSuffix: "02",
description: [
"Src: ./apps/backend",
"Prod: https://api.stack-auth.com",
],
img: "https://www.svgrepo.com/show/340122/datastore.svg",
importance: 2,
},
{
name: "Demo app",
portSuffix: "03",
description: [
"Src: ./examples/demo",
"Prod: https://demo.stack-auth.com",
],
importance: 2,
},
{
name: "Docs",
portSuffix: "26",
description: [
"Src: ./docs",
"Prod: https://docs.stack-auth.com",
],
img: "https://www.svgrepo.com/show/448400/docs.svg",
importance: 2,
},
{
name: "Mintlify docs",
portSuffix: "04",
description: [
"Src: ./docs-mintlify",
],
img: "https://www.svgrepo.com/show/448400/docs.svg",
importance: 2,
},
{
name: "Hosted Components",
portSuffix: "09",
description: [
"Src: ./apps/hosted-components",
],
importance: 2,
},
{
name: "Inbucket",
portSuffix: "05",
img: "https://www.svgrepo.com/show/533176/at-sign.svg",
importance: 1,
description: [
"Email mock",
],
},
{
name: "Prisma Studio",
portSuffix: "06",
importance: 1,
img: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS95TdAw63YPAPcUpvRl4imIf-VJ1sGHnEvbw&s",
description: [
"Database interface",
],
},
{
name: "Jaeger UI (OTel)",
portSuffix: "07",
description: [
"Performance & tracing",
],
importance: 1,
img: "https://www.jaegertracing.io/img/jaeger-icon-reverse-color.svg",
},
{
name: "examples/docs-examples",
portSuffix: "08",
description: [
"Src: ./examples/docs-examples",
],
},
{
name: "examples/cjs-test",
portSuffix: "10",
description: [
"Src: ./examples/cjs-test",
],
},
{
name: "examples/e-commerce",
portSuffix: "11",
description: [
"Src: ./examples/e-commerce",
],
},
{
name: "examples/middleware",
portSuffix: "12",
description: [
"Src: ./examples/middleware",
],
},
{
name: "Svix server",
portSuffix: "13",
importance: 1,
img: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iODQuOSA4NC45IDM0NyAzNDciPgogIDxkZWZzPgogICAgPHN0eWxlPgogICAgICAuY2xzLTEgewogICAgICAgIGZpbGw6ICNmZmY7CiAgICAgIH0KCiAgICAgIC5jbHMtMSwgLmNscy0yIHsKICAgICAgICBzdHJva2Utd2lkdGg6IDBweDsKICAgICAgfQoKICAgICAgLmNscy0yIHsKICAgICAgICBmaWxsOiAjMmM3MGZmOwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8ZyBpZD0iTGF5ZXJfMS0yIiBkYXRhLW5hbWU9IkxheWVyIDEtMiI+CiAgICA8Y2lyY2xlIGNsYXNzPSJjbHMtMSIgY3g9IjI1OC40IiBjeT0iMjU4LjQiIHI9IjE3My41Ii8+CiAgICA8Zz4KICAgICAgPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMzYwLjgsMjMyLjljLTI4LjgtMS40LTU1LjctMTcuMi02OC4yLTQ1LTUuNS0xMi4zLTE3LjgtMjAuNC0zMS4zLTIwLjgtMjguNi0uOC00Ni43LDM1LjEtMjguNyw1Ny42LDYuMiw3LjgsMTUuNCwxMi4xLDI3LjUsMTIuOWgwYzIzLjQsMS43LDQzLjUsMTEuOCw1Ni44LDI4LjUsMjQuNCwzMC43LDIwLjksNzcuMS03LjQsMTA0LTM0LjEsMzIuNS05MS4xLDI1LjgtMTE3LjEtMTMuMi0yLjMtMy41LTQuMy03LjEtNi4xLTEwLjktNS41LTEyLjMtMTcuOC0yMC40LTMxLjMtMjAuOC0xMy45LS40LTI3LDQuOS0zNS4zLDEzLjgsMjcuMyw0Ni45LDc3LjcsNzguNywxMzUuOSw3OS43LDg4LjYsMS41LDE2MS43LTY5LDE2My4yLTE1Ny42LjMtMTQuOS0xLjYtMjkuNC01LjEtNDMuMi43LDIuOS0yMi4zLDEwLjktMjQuOCwxMS42LTkuMiwyLjctMTguNywzLjctMjgsMy4zaDBaIi8+CiAgICAgIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTE1NiwyODMuNmMyOS40LjcsNTYuMSwxOC41LDY4LjIsNDUuMyw1LjUsMTIuMywxNy44LDIwLjQsMzEuMywyMC44LDkuNC4zLDE4LjMtMy4yLDI1LjItOS43LDEyLjgtMTIuMSwxNC41LTM0LjEsMy41LTQ4LTYuMi03LjgtMTUuNC0xMi4xLTI3LjUtMTIuOWgwYy0yMy40LTEuNy00My41LTExLjgtNTYuOC0yOC41LTI5LjYtMzcuMi0xNy05Mi41LDIzLjctMTE1LjQsMTEuOC02LjYsMjUuMi0xMC4yLDM4LjctOS44LDI5LjQuNyw1Ni4xLDE4LjUsNjguMiw0NS4zLDUuNSwxMi4zLDE3LjgsMjAuNCwzMS4zLDIwLjgsMTQsLjQsMjctNC45LDM1LjMtMTMuOC0yNy4zLTQ2LjktNzcuNy03OC43LTEzNS45LTc5LjgtODguNi0xLjUtMTYxLjYsNjkuMS0xNjMuMiwxNTcuNi0uMywxNC45LDEuNiwyOS40LDUuMiw0My4yLDE0LjktMTAuMSwzMy4zLTE1LjYsNTIuOS0xNS4yaDBaIi8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4=",
description: [
"Webhooks",
],
},
{
name: "OAuth mock server",
portSuffix: "14",
description: [
"Src: ./apps/mock-oauth-server",
],
},
{
name: "examples/supabase",
portSuffix: "15",
description: [
"Src: ./examples/supabase",
],
},
{
name: "PgHero",
portSuffix: "16",
description: [
"For database performance analysis",
],
importance: 1,
img: "https://pghero.dokkuapp.com/assets/pghero-88a0d052.png",
},
{
name: "PgHero (Replica)",
portSuffix: "35",
description: [
"For replica database performance analysis",
],
importance: 1,
img: "https://pghero.dokkuapp.com/assets/pghero-88a0d052.png",
},
{
name: "PgAdmin",
portSuffix: "17",
description: [
"For database administration",
],
importance: 1,
img: "https://www.w3schools.com/postgresql/screenshot_postgresql_pgadmin4_6.png",
},
{
name: "Supabase Studio",
portSuffix: "18",
path: "/project/default/editor",
description: [
"For database administration",
],
importance: 1,
img: "https://cdn.prod.website-files.com/655b60964be1a1b36c746790/655b60964be1a1b36c746d41_646dfce3b9c4849f6e401bff_supabase-logo-icon_1.png",
},
{
name: "Drizzle Gateway",
portSuffix: "33",
description: [
"Manage Drizzle configs",
],
importance: 1,
},
{
name: "WAL Info",
portSuffix: "38",
description: [
"Replication & WAL monitoring",
"Tracks primary/replica LSN positions",
"Decodes WAL to SQL statements",
],
importance: 1,
img: "https://www.svgrepo.com/show/374002/replication.svg",
},
{
name: "Internal Tool",
portSuffix: "41",
description: [
"Src: ./apps/internal-tool",
"MCP call review tool",
],
importance: 1,
},
{
name: "Bulldozer Studio",
portSuffix: "39",
description: [
"Bulldozer table graph and editor",
"Includes raw storage debug browser",
],
importance: 1,
img: "https://www.svgrepo.com/show/349299/database.svg",
},
{
name: "JS example",
portSuffix: "19",
description: [
"JavaScript example",
],
},
{
name: "React example",
portSuffix: "20",
description: [
"React example",
],
},
{
name: "ClickHouse HTTP",
portSuffix: "36",
description: [
"ClickHouse",
],
importance: 1,
img: "https://thumbs.bfldr.com/at/qkjfv3nvsv4rbwn94zmtb4t/v/1197417003?expiry=1764357242&fit=bounds&height=800&sig=NjEwNzA0OThjZmJiZDQzZmUwNjIyY2UxYzZiNGYxNmQ3NjJiYjc0OA%3D%3D&width=1100",
},
{
name: "Convex example",
portSuffix: "27",
importance: 0,
description: [
"Convex example",
],
},
{
name: "Lovable React 18 example",
portSuffix: "32",
importance: 0,
description: [
"Lovable React 18 example",
],
},
];
const appsContainers = document.querySelectorAll(".apps-container");
for (let i = 0; i < appsContainers.length; i++) {
const appContainer = appsContainers[i];
const importance = appsContainers.length - i - 1;
for (const app of apps) {
if ((app.importance ?? 0) === importance) {
// TODO escape HTML
appContainer.innerHTML += `
<a href="http://${`${stackPortPrefix}` === "81" ? "" : `p${stackPortPrefix}.`}localhost:${withPrefix(app.portSuffix)}${app.path ?? ""}" target="_blank" rel="noopener noreferrer" class="${app.importance === 2 ? "important" : app.importance === 1 ? "" : "unimportant"}">
<div class="port">:${withPrefix(app.portSuffix)}</div>
<div>
<img src=${app.img || `//localhost:${withPrefix(app.portSuffix)}/favicon.ico`} />
</div>
<span class="description">${app.name}</span>
${app.description ? `<div class="hover-description">${app.description.join("\n")}</div>` : ""}
</a>
`;
}
}
}
</script>
</body>
</html>