Skip to content

Conversation

@thomasgwatson
Copy link
Collaborator

I want to get initial feedback on the migration.

The stripe connect guide allows you to select a few different options to model your overall use-case and then it generates a prompt for a coding assistant. I tweaked the prompt a little more and its generated a lot of code-changes and a lot of TODOs. But so far it looks like a decent start.

I want to be fairly measured about pulling in the changes piece by piece tho, and starting with the migration stuff

Copy link
Collaborator

@tibetsprague tibetsprague left a comment

Choose a reason for hiding this comment

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

looks good so far

table.bigInteger('group_id').unsigned().notNullable().references('id').inTable('groups')
table.bigInteger('product_id').unsigned().references('id').inTable('stripe_products')
table.bigInteger('track_id').unsigned().references('id').inTable('tracks')
table.integer('role_id').unsigned().references('id').inTable('groups_roles')
Copy link
Collaborator

Choose a reason for hiding this comment

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

what is the role for here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A group has three membership options. The basic option just gets you access to the group. The next two options get to access to the group and a specific role. The role is either a cosmetic buff or it actually 'unlocks' some sort of content.

table.string('stripe_session_id', 255)
table.string('stripe_payment_intent_id', 255)
table.integer('amount_paid').defaultTo(0) // 0 for free grants
table.string('currency', 3).defaultTo('usd')
Copy link
Collaborator

Choose a reason for hiding this comment

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

all currencies are 3 characters for sure? what about cryptos?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Looks like it;
https://docs.stripe.com/currencies

I have seen some reference to users being able to pay with crypto but its not on the currencies page (?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As far as I understand at the moment, these aspects of the transaciton info in our database will not the source of truth; that will be the transaction on stripe's side.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

And to this end, I have actually removed them. We will list transactions in our interface but basically all details for them will stay with stripe and we would just link to them from our UI

@thomasgwatson thomasgwatson changed the title Initial draft of db migration Initial draft of paid content backend Oct 23, 2025

This document provides a comprehensive walkthrough of the entire paid content workflow, from admin setup to user purchase completion.

## Table of Contents
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@tibetsprague give this a look and see if it makes sense to you as a workflow

@thomasgwatson
Copy link
Collaborator Author

@tibetsprague ok, so here is another round of changes.

A few updates to the migration:

  1. function to sync content with the expired_at value of its content_access record
  2. Added expired_at to the various pieces of content that can be gated, for quick reference when we are checking access through the platform
  3. added json field on products to define what things are permitted when someone buys something

Also added a workflow explainer markdown file, that you should check to make sure the workflow sounds sensible

Copy link
Collaborator

@tibetsprague tibetsprague left a comment

Choose a reason for hiding this comment

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

looking good so far!

}

// Find the group with this Stripe account ID
const group = await Group.where({ stripe_account_id: account.id }).fetch()
Copy link
Collaborator

Choose a reason for hiding this comment

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

can multiple groups use the same stripe account? probably. i wonder if there is another identifier for like a sub-account or something like that? or maybe they just give separate account_ids for each one.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ooo right that is an interesting question. This is a webhook, so I will have to check if we can store a 'foreign key' of sorts from our db in their records for an account. I assume we can

}

// Check if group already has a Stripe account
// TODO STRIPE: You may want to store the accountId in your Group model
Copy link
Collaborator

Choose a reason for hiding this comment

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

ok so it seems like each group will have its own "account"

model: MemberGroupRole,
attributes: [
'id',
'expires_at',
Copy link
Collaborator

Choose a reason for hiding this comment

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

intriguing, so this would be for a product that gives you a certain role?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For example: for the astrology group, they define three tiers of membership with different subscription rates.

So each of those tiers could provide a role/badge that is unique to them. I don't think we have it specced at the moment but in the future this could be used to gate chat rooms or perhaps even specific content or be another way to unlock access to a track. And those role grants don't need to be paid for either; in general, groups might want to restrict content to specific roles

type Track {
id: ID
# Whether this track requires purchased access (paid content)
accessControlled: Boolean
Copy link
Collaborator

Choose a reason for hiding this comment

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

seems like we should call this the same thing we call it on group, so either both are paywall (or paywalled?) or both are accessControlled. i lean towards paywalled

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So groups already have a property and concept related to restricted access but tracks don't. So the properties, despite both being booleans and having some conceptual overlap, don't have the same scope. So I don't want to give them the same name and thus infer that they have the same scope.

The doc note here is outdated; admins can grant access without a payment or stripe being involved.

}
}

// Add role-specific access records
Copy link
Collaborator

Choose a reason for hiding this comment

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

still a little unclear on this. this means that everyone with this role gets access to this thing? or that people with this content get this role?

**When access is revoked or expires:**
1. Status changes to 'revoked' or 'expired' in `content_access`
2. Trigger `content_access_expires_at_clear` fires
3. Function `clear_content_access_expires_at()` executes:
Copy link
Collaborator

Choose a reason for hiding this comment

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

so it always has an expires_at value if content is accessible? and for lifetime we just put one far in the future?

// Creates: content_access { user_id, group_id, expires_at: '2026-01-01', track_id: NULL }
// Trigger updates: group_memberships.expires_at = '2026-01-01' ✓

// Step 2: Same user buys 1-month access to Track A (3 months later)
Copy link
Collaborator

Choose a reason for hiding this comment

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

i think racks will always be lifetime?

- Email includes:
- Purchase confirmation details
- Links to access the content they purchased
- Instructions for accessing their new content
Copy link
Collaborator

Choose a reason for hiding this comment

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

a special group link that sets up the access? or already they will have been marked as being able to access the group in the database so then its just the normal group link

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

maybe "specific" is a better word than "special" here; a specific link to the content.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So, this is worth checking in about and there is still a bit of gray area based on exactly how the stripe hand-off goes:

Say, we are a user, in the stripe checkout and I hit purchase.

  • I think then stripe redirects the user to the successUrl that we provide.
  • Meanwhile, there is an async update between Stripe (via webhook) and us, letting us know that the payment worked out
  • So, I am thinking that we made the successUrl pretty generic and simple and more or less just say: check your email

this saves us if there is a significant lag between transaction and webhook update.... but if it turns out that turn-around time is really fast (consistently?) then we are just adding another step (checking the email) for the user...

Any initial thoughts? Might just be something we figure out once we start seeing it implemented

@gitguardian
Copy link

gitguardian bot commented Jan 7, 2026

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

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.

4 participants