Skip to content

Implement private org creation with Stripe integration#7

Open
gulfaniputra wants to merge 10 commits intosurprisetalk:mainfrom
gulfaniputra:create-org
Open

Implement private org creation with Stripe integration#7
gulfaniputra wants to merge 10 commits intosurprisetalk:mainfrom
gulfaniputra:create-org

Conversation

@gulfaniputra
Copy link
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
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.

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
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
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.

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
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
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.

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
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
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.

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
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
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.

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
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
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.

server.tsx Outdated
Comment on lines +1038 to +1039
insert into org (name, created_by, stripe_sub_id)
values (${orgName}, ${creatorName}, ${subId})
Copy link
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
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.

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
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
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.

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
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
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
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