PCI Compliance Guide
Explains PCI DSS compliance via tokenization, lists forbidden and allowed data, and provides code examples for BigCommerce and Stripe.
What this file does
Explains PCI DSS compliance via tokenization, lists forbidden and allowed data, and provides code examples for BigCommerce and Stripe.
When to use it
- Implementing payment method storage in a web app
- Auditing an existing payment flow for PCI violations
- Training developers on safe card data handling
- Setting up tokenization with BigCommerce or Stripe
Assumes this stack
PCI Compliance Guide
How payment methods are handled in a PCI DSS compliant manner using tokenization. For QuickBooks payment integration, see QUICKBOOKS_FEATURES.md. For quick start, see PAYMENT_METHODS_QUICK_START.md.
Critical Security Rules
⚠️ NEVER Do These Things
- NEVER store full credit card numbers - Only store last 4 digits
- NEVER store CVV/CVC codes - Not even encrypted
- NEVER store full bank account numbers - Only store last 4 digits
- NEVER store full magnetic stripe data
- NEVER store PIN numbers or PIN blocks
- NEVER log or console.log payment data
- NEVER transmit card data over unencrypted connections
- NEVER create custom payment forms that collect card data
✅ What You CAN Store
According to PCI DSS, you may store:
- Last 4 digits of card/account number
- Cardholder/account holder name
- Card expiration date
- Service code
- Payment processor tokens (our primary method)
Our PCI Compliance Strategy
We use tokenization to achieve PCI compliance:
┌─────────────┐
│ Browser │
│ │
│ Customer │
│ enters │
│ card data │
└──────┬──────┘
│
│ Sent directly to payment processor
│ (NEVER to our server)
↓
┌─────────────────────────────┐
│ Payment Processor │
│ (BigCommerce/Stripe/etc) │
│ │
│ - Validates card │
│ - Creates secure token │
└──────┬──────────────────────┘
│
│ Returns token
↓
┌─────────────┐
│ Our Server │
│ │
│ Stores only:│
│ - Token │
│ - Last 4 │
│ - Expiry │
│ - Name │
└─────────────┘
Implementation Details
Database Schema
The payment_methods table stores:
CREATE TABLE payment_methods (
id UUID PRIMARY KEY,
organization_id UUID NOT NULL,
location_id UUID,
user_id UUID NOT NULL,
-- Display Information (PCI Compliant)
label TEXT NOT NULL, -- e.g., "Corporate Card"
payment_type TEXT NOT NULL, -- credit_card, debit_card, bank_account, ach
last_four TEXT NOT NULL, -- Only last 4 digits
account_holder_name TEXT NOT NULL, -- Name on card/account
-- Card-specific (optional)
expiry_month INTEGER, -- Expiration month
expiry_year INTEGER, -- Expiration year
-- Bank-specific (optional)
bank_name TEXT, -- Bank name
account_type TEXT, -- checking or savings
-- Payment Processor Integration
payment_token TEXT, -- Tokenized reference (NOT raw data)
payment_processor TEXT NOT NULL, -- e.g., 'bigcommerce', 'stripe'
-- Metadata
is_default BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Row Level Security
RLS policies ensure:
- Users can only view payment methods for their organizations
- Only organization admins can add/update/delete payment methods
- Payment tokens are never exposed unnecessarily
Integration Guide
Step 1: Tokenize Payment Data
Use the payment processor's SDK to tokenize payment data client-side:
Example: BigCommerce Payments
// Load BigCommerce Payments SDK
const paymentsClient = window.BigCommerce.payments.client.create({
storeId: 'YOUR_STORE_ID',
currencyCode: 'USD'
});
// Tokenize card data (happens in browser)
async function tokenizeCard(cardData) {
try {
const token = await paymentsClient.tokenizeCard({
number: cardData.number, // Full card number (client-side only)
cvv: cardData.cvv, // CVV (client-side only)
expiryMonth: cardData.expiryMonth,
expiryYear: cardData.expiryYear,
cardholderName: cardData.name
});
// Token is safe to send to your server
return {
token: token.id,
lastFour: cardData.number.slice(-4),
expiryMonth: cardData.expiryMonth,
expiryYear: cardData.expiryYear,
name: cardData.name
};
} catch (error) {
console.error('Tokenization failed:', error);
throw error;
}
}
Example: Stripe
// Load Stripe.js
const stripe = Stripe('YOUR_PUBLISHABLE_KEY');
const elements = stripe.elements();
const cardElement = elements.create('card');
// Tokenize card data
async function tokenizeCard() {
const { token, error } = await stripe.createToken(cardElement);
if (error) {
console.error('Tokenization failed:', error);
throw error;
}
return {
token: token.id,
lastFour: token.card.last4,
expiryMonth: token.card.exp_month,
expiryYear: token.card.exp_year,
name: token.card.name,
cardType: token.card.brand
};
}
Step 2: Save Token to Database
import { addPaymentMethod } from '@/services/paymentMethods';
async function savePaymentMethod(tokenizedData, organizationId) {
const { data, error } = await addPaymentMethod({
organization_id: organizationId,
label: 'Primary Card',
payment_type: 'credit_card',
last_four: tokenizedData.lastFour, // Safe to store
expiry_month: tokenizedData.expiryMonth, // Safe to store
expiry_year: tokenizedData.expiryYear, // Safe to store
account_holder_name: tokenizedData.name, // Safe to store
payment_token: tokenizedData.token, // Tokenized reference
payment_processor: 'bigcommerce',
is_default: true
});
if (error) {
console.error('Failed to save payment method:', error);
return;
}
console.log('Payment method saved successfully');
}
Step 3: Use Token for Payments
When processing a payment, use the stored token:
import { getDefaultPaymentMethod } from '@/services/paymentMethods';
async function processPayment(organizationId, amount) {
// Get the tokenized payment method
const { data: paymentMethod, error } = await getDefaultPaymentMethod(organizationId);
if (error || !paymentMethod) {
throw new Error('No payment method found');
}
// Use the token with the payment processor
// The processor will handle the actual payment using the token
const paymentResult = await paymentProcessorAPI.charge({
token: paymentMethod.payment_token,
amount: amount,
currency: 'USD'
});
return paymentResult;
}
Bank Accounts (ACH)
For bank accounts, follow the same tokenization process:
async function tokenizeBankAccount(bankData) {
const token = await paymentsClient.tokenizeBankAccount({
accountNumber: bankData.accountNumber, // Client-side only
routingNumber: bankData.routingNumber, // Can be stored (public)
accountType: bankData.accountType, // checking or savings
accountHolderName: bankData.name
});
return {
token: token.id,
lastFour: bankData.accountNumber.slice(-4),
accountType: bankData.accountType,
name: bankData.name,
bankName: bankData.bankName
};
}
Save to database:
await addPaymentMethod({
organization_id: organizationId,
label: 'Business Checking',
payment_type: 'bank_account',
last_four: tokenizedData.lastFour,
account_holder_name: tokenizedData.name,
bank_name: tokenizedData.bankName,
account_type: 'checking',
payment_token: tokenizedData.token,
payment_processor: 'bigcommerce',
is_default: true
});
Security Checklist
- All payment data tokenized before reaching server
- CVV codes never stored
- Full card numbers never stored
- Full account numbers never stored
- Payment forms use payment processor SDKs
- HTTPS enabled on all endpoints
- Row Level Security enabled on payment_methods table
- Payment tokens stored securely
- Access restricted to authorized users only
- Regular security audits performed
Testing
Test Card Numbers (BigCommerce)
For development/testing, use these test card numbers:
Visa: 4111 1111 1111 1111
Mastercard: 5500 0000 0000 0004
Amex: 3400 0000 0000 009
Discover: 6011 0000 0000 0004
Any future expiry date
Any 3-digit CVV (4 digits for Amex)
Test Bank Account (BigCommerce)
Routing Number: 110000000
Account Number: 000123456789
Account Type: checking
Common Mistakes to Avoid
❌ DON'T: Create custom payment forms
// WRONG - This collects card data on your server
<input type="text" name="card_number" />
<input type="text" name="cvv" />
✅ DO: Use payment processor forms
// CORRECT - Use processor's secure form
<div id="bigcommerce-payment-form"></div>
// or
<div id="stripe-card-element"></div>
❌ DON'T: Store sensitive data
// WRONG - Storing full card number
await supabase.from('payment_methods').insert({
card_number: '4111111111111111', // PCI VIOLATION
cvv: '123' // PCI VIOLATION
});
✅ DO: Store only tokens
// CORRECT - Only storing token and safe display data
await supabase.from('payment_methods').insert({
payment_token: 'tok_abc123xyz', // Safe
last_four: '1111', // Safe
expiry_month: 12, // Safe
expiry_year: 2025 // Safe
});
Related Documentation
- PAYMENT_METHODS_QUICK_START.md - Payment methods quick start guide
- QUICKBOOKS_FEATURES.md - QuickBooks features overview
- QUICKBOOKS_INTEGRATION.md - QuickBooks technical integration
- PCI DSS Quick Reference Guide
- BigCommerce Payments API
- Stripe Security Best Practices
- PCI Tokenization Guidelines
What's inside
8 sections: security rules, tokenization diagram, DB schema, RLS notes, integration steps (3), ACH handling, checklist, test data, and common mistakes.
Change this for your project
- Replace
'YOUR_STORE_ID'with your BigCommerce store ID - Replace
'YOUR_PUBLISHABLE_KEY'with your Stripe publishable key - Replace
@/services/paymentMethodsimport path with your own service module - Replace
'bigcommerce'processor string with your actual processor name
Where it goes
A standard operating procedure. Keep where the team or agent running the process will find it.
Worth borrowing
- Client-side tokenization diagram showing data never touches your server
- Security checklist that doubles as a deployment gate
- Explicit 'NEVER' rules alongside 'what you CAN store' for clear developer guardrails
Related Documents
Comprehensive AI Assistant Tools Reference
Lists 80+ tools with MCP server associations, bulk support, parallel capability, resource impact, and execution type for AI agent workflows.
iOS Deployment Guide
Walks through setting up an iOS development environment, building a Tauri app for iOS, and publishing to the App Store or alternative channels.
How to Add Resources to Your FastMCP Server
Teaches how to add static and dynamic MCP resources to a FastMCP server, with six ready-to-copy examples for a GitHub crawler.
Continue.dev MCP Integration Setup Guide
Walks through configuring Continue.dev to connect an MCP server for spatial transcriptomics tasks, with local and remote setup options.