Skip to content

Implement private org creation with Stripe integration#7

Merged
surprisetalk merged 10 commits intosurprisetalk:mainfrom
gulfaniputra:create-org
Apr 9, 2026
Merged

Implement private org creation with Stripe integration#7
surprisetalk merged 10 commits intosurprisetalk:mainfrom
gulfaniputra:create-org

Conversation

@gulfaniputra
Copy link
Copy Markdown
Contributor

Summary: Implements private organization creation and management with Stripe integration at $1/member/month.

Demo: Screencast

Changes:

  • Database: Added org table for subscriptions and ownership.
  • Frontend: Added OrgCreate and OrgDashboard components.
  • Backend: Added Stripe Checkout flow and member management (/invite & /remove).

Integration Tests: Passed 4/4 steps (4s) using pglite and Stripe mocks (terminal output).

Copy link
Copy Markdown
Owner

@surprisetalk surprisetalk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work! The architecture is very well designed. I added some small stylistic suggestions.

Comment thread db.sql Outdated
create table org (
name citext primary key check (name ~ '^[0-9a-zA-Z_]{4,32}$'),
created_by citext references usr (name) not null,
stripe_sub_id text,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this a unique column

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I’ve added unique to stripe_sub_id. Stripe IDs are globally unique so this should be enforced at the DB level.

Comment thread org_test.ts Outdated
import { PostgresConnection } from "pg-gateway";
import dbSql from "./db.sql" with { type: "text" };

const pglite = (f: (sql: pg.Sql) => (t: Deno.TestContext) => Promise<void>) => async (t: Deno.TestContext) => {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might just want to append the org tests to server.test.ts so we don't have to copy/paste this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I’ve moved the org tests into server.test.ts. I’ve also added jane_doe seed and Stripe mock to the shared pglite helper.

Comment thread org_test.ts Outdated
Comment on lines +43 to +53
await db.exec(`
insert into usr (name, email, password, bio, email_verified_at, invited_by, orgs_r, orgs_w)
values ('john_doe', 'john@example.com', 'hashed:password1!', 'sample bio', now(), 'john_doe', '{secret}', '{secret}')
on conflict do nothing;
`);

await db.exec(`
insert into usr (name, email, password, bio, email_verified_at, invited_by, orgs_r, orgs_w)
values ('jane_doe', 'jane@example.com', 'hashed:password1!', 'sample bio', now(), 'john_doe', '{}', '{}')
on conflict do nothing;
`);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can add these to db.sql and the tests should load them automatically

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. I’ve moved the seed inserts into db.sql so pglite picks them up automatically via the schema load.

Comment thread server.tsx Outdated
Comment on lines +76 to +80
tag: tokens.filter(t => t.startsWith("#")).map(t => t.slice(1).toLowerCase()),
org: tokens.filter(t => t.startsWith("*")).map(t => t.slice(1).toLowerCase()),
usr: tokens.filter(t => t.startsWith("@")).map(t => t.slice(1)),
www: tokens.filter(t => t.startsWith("~")).map(t => t.slice(1).toLowerCase()),
text: tokens.filter(t => !/^[#*@~]/.test(t)).join(" "),
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see a lot of formatting changes. All files should be formatted with deno fmt before commit. Double-check that prettier isn't overriding

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Root cause was Prettier overriding deno fmt on save. Added .prettierignore to exclude .ts/.tsx from Prettier and .editorconfig for baseline consistency across editors.

Comment thread server.tsx Outdated
Comment on lines 505 to 507
return isImage
? \`<a href="\${url}">\${url}</a><br><img src="\${url}" loading="lazy" style="max-width:100%;max-height:400px;">\`
: \`<a href="\${url}">\${url}</a>\`;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this block broke my deno fmt when I tried to run it 😅 Going to push a fix for that rn

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pulled in your fix and ran deno fmt. Formatting is clean now.

Comment thread server.tsx Outdated
Comment on lines +1038 to +1039
insert into org (name, created_by, stripe_sub_id)
values (${orgName}, ${creatorName}, ${subId})
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be easier to do this:

insert into org ${sql({ name: orgName, created_by: creatorName, stripe_sub_id: subId })}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use the sql({}) object syntax for consistency with the rest of the queries in this file.

Comment thread server.tsx Outdated
Comment on lines +1042 to +1046
await sql`
update usr
set orgs_r = array_append(orgs_r, ${orgName}),
orgs_w = array_append(orgs_w, ${orgName})
where name = ${creatorName}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using a transaction, you can use a CTE in a single query like this:

with o as (
  insert into org ...
)
update usr
set orgs_r = ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the transaction with a single CTE. Cleaner and still atomic.

Comment thread server.tsx Outdated
sql`select name from usr where ${c.req.param("name")} = any(orgs_r)`,
]);
if (!org) return notFound();
if (!viewerOrgs.includes(org.name)) throw new HTTPException(403, { message: "Access denied" });
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of fetching that viewerOrgs array, you can also do something like

select true
from usr
where true
  and name = ${c.get("name") ?? ""}
  and ${c.req.param("name")} = any(orgs_r)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the orgs_r array fetch with a direct select true membership check in the db.

- Add stripe to imports
- Seed jane_doe in shared pglite helper
- Add stripe mock to shared pglite helper
- Append org management test block
- org tests moved to 'server.test.ts'
- Seed data moved to 'db.sql'
- No logic lost
- Prevent Prettier from overriding 'deno fmt' on .ts/.tsx files
- Add baseline editor consistency across editors
- Insert into org
- Update usr orgs_r & orgs_w
@surprisetalk surprisetalk merged commit 4db176b into surprisetalk:main Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants