Privacy compliance for Australian small business is misunderstood. Most founders think it's a legal checkbox: tick a box, add a privacy policy, ship it. But regulators—the Office of the Australian Information Commissioner (OAIC) and the EU Data Protection Board—care about *architecture*, not paper. If your database design doesn't enforce privacy, no policy document will save you.
Velocity X builds privacy defaults into the application layer. Consent capture with timestamps, IP logging, and version pinning. Soft-delete with automatic 30-day retention purge. Row-level security for access control. Encrypted tokens at rest. OAuth with minimum scoping. Not as a bolt-on compliance module, but woven into schema and business logic from day one.
Australian Privacy Principles + GDPR: The Overlap for Small Business
The Australian Privacy Act 1988 requires organisations to collect, use, and disclose personal information lawfully, fairly, and transparently. The EU's GDPR is stricter and applies to any European user data you touch. If you're an Australian SaaS with even one EU customer, GDPR is live. Here's what both regimes demand:
Consent (explicit + versioned). You can't scrape email addresses from LinkedIn and assume consent. Both frameworks require documented, informed consent that users can withdraw. Velocity X records consent at signup with a timestamp, IP address, and consent-version hash so you can prove which terms the user agreed to on which date.
Data minimisation. Collect only what you need. Velocity X enforces this at the form level: only required fields are submitted; optional fields are flagged in schema.
Retention limits. You can't store data forever "just in case". GDPR mandates deletion timelines. The Australian Privacy Principles say data shouldn't be kept longer than necessary. Velocity X defaults to 30-day soft-delete: data is marked deleted on request, then hard-purged after 30 days. No manual cleanup required.
Access control & audit logs. You must document who accessed what data, when, and why. GDPR's "right to access" means users can request a copy of everything you hold. Velocity X uses Postgres row-level security (RLS) with triggers to log every data access by user, timestamp, and action. Users can export their data via an API endpoint.
Encryption at rest + in transit. Personal data must be protected from unauthorised access. Velocity X encrypts sensitive fields (tokens, passwords) using AES-256-GCM at the database layer, not just on the wire.
Consent Capture: Timestamp, IP, Version
Here's the schema Velocity X uses for consent:
{`create table user_consents (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users(id) on delete cascade,
consent_type text not null, -- 'terms', 'privacy', 'marketing'
version text not null, -- semantic version of the policy document
given_at timestamp default now(),
given_from_ip inet, -- captured at signup
given_from_user_agent text, -- browser + OS fingerprint
withdrawn_at timestamp,
created_at timestamp default now()
);
create index on user_consents(user_id, consent_type);`}
On signup, your form captures consent and logs it with a given_at timestamp, the user's IP, and the version of your terms they're agreeing to. If a user requests deletion, you can prove what they consented to and when. If you update your privacy policy, the old version remains tied to users who signed up under it. Regulators love this.
The API endpoint that records consent looks like:
{`-- POST /api/auth/consent
const { data: consent } = await supabase
.from('user_consents')
.insert({
user_id: user.id,
consent_type: 'privacy',
version: '2026-06-01', // must match published version
given_from_ip: request.headers.get('x-forwarded-for'),
given_from_user_agent: request.headers.get('user-agent'),
})
.select()
.single();
// Emit event for marketing automation
emit('user.consented', { user_id: user.id, consent_type });`}
Soft-Delete + 30-Day Retention Purge
When a user hits the "delete my account" button, data doesn't vanish immediately. That's reckless—you can't comply with GDPR's "right to erasure" if you've corrupted your own audit trail. Instead, Velocity X marks data as deleted and schedules hard-deletion 30 days later.
{`create table users (
id uuid primary key,
email text not null,
name text,
deleted_at timestamp, -- soft-delete marker
deleted_reason text, -- 'user_request', 'compliance', 'abuse'
created_at timestamp default now()
);
-- Add a view for "live" users only
create view live_users as
select * from users where deleted_at is null;
-- Trigger: auto-delete after 30 days
create or replace function purge_deleted_users()
returns void as $$
begin
delete from users
where deleted_at < now() - interval '30 days';
end;
$$ language plpgsql;
-- Schedule via pg_cron
select cron.schedule(
'purge_deleted_users_daily',
'0 2 * * *', -- 2am daily
'select purge_deleted_users();'
);`}
When the user requests deletion, you update deleted_at = now(). Your app queries only live_users so deleted data is invisible immediately. After 30 days, the cron job hard-deletes rows. GDPR auditors will ask: "Show us your retention period." You show them the cron job and the deleted_at column. Tick.
Encryption at Rest: AES-256-GCM Tokens
OAuth tokens, API keys, and passwords must never be stored in plaintext. Velocity X uses Postgres's bytea type to store AES-256-GCM encrypted values directly in the database:
{`-- Store encrypted token in bytea column
create table oauth_tokens (
id uuid primary key,
user_id uuid references auth.users(id),
provider text, -- 'google', 'github'
token_encrypted bytea not null, -- AES-256-GCM ciphertext
token_nonce bytea not null, -- AEAD nonce (unique per encryption)
created_at timestamp default now()
);
-- On insert: encrypt before store
create or replace function encrypt_oauth_token()
returns trigger as $$
declare
nonce bytea;
encrypted bytea;
begin
nonce := gen_random_bytes(12); -- AES-GCM nonce
encrypted := pgcrypto.encrypt_aes_gcm(
new.raw_token,
md5(current_setting('app.encryption_key'))::bytea,
nonce
);
new.token_encrypted := encrypted;
new.token_nonce := nonce;
new.raw_token := null; -- clear plaintext
return new;
end;
$$ language plpgsql security definer;
create trigger encrypt_token_on_insert
before insert on oauth_tokens
for each row execute function encrypt_oauth_token();`}
The encryption key lives in environment variables and never appears in code. When you need to decrypt—e.g., refreshing a Google token—you decrypt in memory, use it, and discard. If an attacker steals your database backup, they get ciphertext, not tokens. See `/blog/postgres-bytea-aes-256-gcm-token-encryption` for the full implementation.
Access Logs via Postgres Triggers
GDPR's "right to access" means users can request every piece of personal data you hold about them, including when it was accessed. Velocity X logs data access at the Postgres layer using triggers:
{`create table data_access_log (
id bigserial primary key,
user_id uuid not null,
data_subject uuid not null, -- whose data was accessed
table_name text not null,
action text, -- 'select', 'update', 'delete'
old_values jsonb,
new_values jsonb,
accessed_by text, -- JWT claim: email or service
accessed_at timestamp default now()
);
-- Trigger on any user-data table
create or replace function log_user_data_access()
returns trigger as $$
begin
insert into data_access_log (
user_id, data_subject, table_name, action, old_values, new_values, accessed_by
) values (
(auth.jwt() -> 'sub')::uuid,
coalesce(new.id, old.id),
tg_table_name,
tg_op,
to_jsonb(old),
to_jsonb(new),
auth.jwt() -> 'email'
);
return coalesce(new, old);
end;
$$ language plpgsql security definer;
create trigger log_access_users after select, insert, update, delete on users
for each row execute function log_user_data_access();`}
Now every query is logged. When a data subject requests an access report, you query data_access_log for their ID and spit out a JSON dump. GDPR wants proof of what data you hold and who saw it. Done.
Row-Level Security for Access Control
Velocity X uses Supabase's row-level security (RLS) to ensure users can't peek at other users' data via a compromised API key or Edge Function. Every table has a policy:
{`alter table users enable row level security;
create policy "users_see_only_themselves" on users
for select
using (id = auth.uid());
create policy "users_can_update_themselves" on users
for update
using (id = auth.uid());
create policy "admins_see_all" on users
for all
using (
(auth.jwt() -> 'role')::text = 'admin'
);`}
No middleware, no application-layer auth check can bypass this. The database itself enforces it. If a function tries to query user emails, Postgres returns only rows the user can see.
OAuth Minimum Scoping
When integrating Google, GitHub, or Stripe OAuth, only request scopes you actually use. Velocity X defaults to the narrowest scopes:
Google: openid profile email (not calendar, drive, gmail). GitHub: read:user user:email (not repos, gists, organisations). Stripe: Webhook signing only (not secret API keys on the client).
Regulators ask: "Why do you need access to my calendar?" If you don't, remove the scope. Data minimisation starts with OAuth scopes.
Frequently Asked Questions
Do I have to comply with GDPR if I'm Australian?
If you have even one EU customer, yes. GDPR applies extraterritorially. You might be an all-Australian business with one European user; that one user triggers full GDPR compliance for their data. Build it in from day one rather than retrofitting it later when you hit your first EU customer.
What about Australia's Privacy Act vs. GDPR? Can I use different rules?
No. If you process both Australian and EU data, apply the stricter framework (GDPR) to all users. It's simpler than maintaining two code paths. GDPR's 30-day deletion, consent versioning, and access logs are habits worth having.
Is soft-delete required?
Not legally, but it's safer. Hard-delete is irreversible; soft-delete lets you recover from mistakes. If a user requests deletion mid-transaction, soft-delete buys you 30 days to audit before purging. Hard-delete is the nuclear option—use it only after the 30-day window.
Can I log access to my own data as an admin?
Yes. RLS policies typically exempt admins and internal services. But *those accesses* still appear in the audit log. You can explain: "Our support team accessed your account on June 12 to debug a payment issue." Transparency wins privacy audits.
What if I'm using a third-party API (Stripe, Twilio) for PII processing?
You're a data processor, and they're a sub-processor. GDPR requires data processing agreements (DPAs) signed with both parties. Most SaaS vendors (Stripe, Twilio, AWS) provide pre-signed DPAs. Get them in writing before storing data with them. Velocity X defaults to API-only (no customer data synced to third parties except explicitly authorised payments).
What's the difference between soft-delete and anonymisation?
Soft-delete is temporary; data is still identifiable (just marked deleted). Anonymisation is permanent; you hash emails, randomise names, strip IPs. GDPR prefers deletion but accepts anonymisation if you prove it's irreversible. Velocity X soft-deletes first, then optionally anonymises after the 30-day window if the user consents to aggregate analytics.
The Bottom Line
Privacy compliance isn't a feature. It's architecture. Velocity X treats it as such: consent versioning, soft-delete retention, RLS access control, encrypted tokens, and audit trails are embedded in schema design. When an OAIC or GDPR auditor asks "How do you enforce retention?", you show them the cron job. When they ask "Who accessed this user's data?", you query the audit log. When they ask "Can the user export everything?", you hit an API endpoint. No scrambling. No retrofitting. Just ship with privacy baked in.