Review request

Tattoo studio deposits, is it wrong that I store the card number myself?

Solved · 3105 viewsasked by sam_builds

Repo or live app

git.example.com/samokafor/ink-deposits ↗

Unsure about: Security

Clients pay a 50 euro deposit when they book. Claude Code built a form that posts the card number to my API, saves it in a payment_methods table so the artist can charge the rest on the day, and then calls Stripe. It works in test mode. Something about it feels off and I cannot articulate why.

Comment
Please stop storing those today and drop the table. With Checkout or Elements the number never reaches your server at all. tobiasw

2 answers

Marked as helpful by the asker
priya_ships

Your instinct is right and this is the one thing you should stop today. The moment a raw card number touches your server you are in PCI scope: annual assessment, quarterly scans, real liability if that table leaks. Nobody at your size wants that.

The supported pattern keeps the number out of your stack entirely. Collect it with Stripe Elements or Checkout in the browser, create a Customer, and save the card with a SetupIntent using usage: 'off_session'. You store the customer id and the payment method id, both harmless strings. On the day of the appointment you charge with:

stripe.paymentIntents.create({
  amount: rest, currency: 'eur', customer, payment_method,
  off_session: true, confirm: true
})

Then drop the payment_methods columns and rotate nothing, because you never had anything worth rotating.

Comment
Dropped the table and moved to a SetupIntent. Thank you for not being mean about it. sam_builds
Everyone builds the thing that feels logical. Glad you asked before a real card went in. priya_ships
Check your server logs and error tracking too. Request bodies with card numbers love to end up there. lena_ops
Logs had four test cards in them. Purged. sam_builds
femi_o

One thing to check after the switch: the deposit and the saved card should be one flow, not two. If you take the 50 euro with a PaymentIntent and then run a separate SetupIntent, clients get two bank confirmation screens and some drop off at the second.

Create the deposit PaymentIntent with setup_future_usage: 'off_session'. One confirmation, the deposit is charged, and the card is saved for the rest on the day.

Then handle authentication_required on the final charge, because some banks ask again anyway, and send the client a link to pay the rest when that happens.

Comment
It was two flows, and yes, two 3D Secure screens on my own card. Merged into one PaymentIntent. sam_builds
setup_future_usage is the right call. Test with the 3D Secure test cards too, they behave differently from 4242. tobiasw