I was wondering if there was some kind of "convention" or otherwise a best practice for storing Stripe payment data in the database. There is a real lack of information online and since I am doing it on my own I wanted to get some feedback from others.
The basic setup of my site is that there is a store with several products. There is also an option to subscribe, to receive these products monthly.
So I set up two post routes. One for buying products, and one for subscribing. These use the Stripe API. For orders, I use stripe.charge
, and for subscriptions, I create a customer from stripe.customer
and subscribe them to a plan.
For stripe.charges
, I save the object returned into the database. This contains all of the charge data. There is a webhook sent as charge.succeeded
, if the charge succeeded
. I do not store this data. Maybe I should just store the charge id here, instead of the entire charge object. I store it as below:
ORDER ROUTE
module.exports = async (req, res) => {
try {
const charge = await stripe.charges.create({
amount: req.session.cart.totalPrice,
currency: 'usd',
source: req.body.id
});
const order = await new Order({
user: req.user,
cart: req.session.cart,
stripeCharge: charge
}).save();
req.session.cart = new Cart({});
res.sendStatus(200);
} catch (e) {
console.log(e);
}
};
With subscriptions, it’s a little more complex, because the created customer object does not contain any charge id. So I save the customer id to that user model. Stripe fires 6 webhooks if the subscription is processed fine. I store customer.subscription.created
, customer.created
, charge.succeeded
, and invoice.created
. I store the customer object like this:
SUBSCRIPTION ROUTE
module.exports = async (req, res) => {
try {
if(!req.user.hasSubscription) {
const customer = await stripe.customers.create({
email: req.body.email,
source: req.body.id,
plan: 'pro'
});
await new Subscription({
user: req.user,
customerId: customer.id
}).save();
req.user.hasSubscription = true;
await req.user.save();
res.send(req.user);
}
} catch (e) {
console.log(e);
}
};
As you can see, I have a boolean set up on my Mongoose User
model, hasSubscription
. If this is true, no customer is created, and so no subscription is set up. If it is false, I save the customer id from the customer object created. Then in the webhook route, I save the 4 events above to the correct user, matching by customer id. I could get away with saving less data here I think, possibly making a record of any subscriptions, and any cancellations. I save the webhook events as below:
WEBHOOKS ROUTE
module.exports = async (req, res) => {
try {
const data = {};
if (req.body.type === 'customer.subscription.created') {
await Subscription.findOneAndUpdate({ customerId:
req.body.data.object.customer }, {
$set: {
'stripe_data.customer_subscription_created':
req.body.data.object
}
}, {
new: true
});
res.sendStatus(200);
};
…//OTHER STRIPE EVENTS SIMILAR TO ABOVE…
}
I am storing more data than I likely need just to be on the safe side, and I read somewhere that suggested storing adequate data was good for legal compliance. But since Stripe stores everything in the dashboard, I can see your point that a simple piece of identifying information like the charge id would be enough.
I am still in test mode, but I think Stripe only sends back the last 4 digits of the card, and the expiry year like so:
exp_year: 2042
last4: '4242'
With regards to saving confidential information to the database: I do not see any more sensitive information being exposed. The card itself is referenced by an id that Stripe creates.