Working with UUIDs: A Guide to Secure Primary Keys in PostgreSQL
Learn how to replace predictable serial IDs with secure UUIDs in PostgreSQL. Discover how to enable pgcrypto, generate unique keys, and harden your database.

Previously in this course, we discussed the mechanics of Understanding Primary Keys: Ensuring Data Integrity in PostgreSQL. While SERIAL integers are convenient for small projects, they are predictable and can expose sensitive information about your growth. In this lesson, we replace those integer-based keys with UUIDs (Universally Unique Identifiers) to improve scalability and boost Database security.
Why Move Beyond Serial IDs?
A SERIAL type simply counts up: 1, 2, 3... This is "predictable." If a user visits store.com/orders/50, they can easily guess that store.com/orders/51 exists. This is a common vector for Database security vulnerabilities like ID enumeration attacks.
Furthermore, when you scale across multiple database servers, merging two tables with SERIAL keys causes primary key collisions. UUIDs solve this by providing a 128-bit value that is statistically guaranteed to be unique across time and space without requiring a central coordinator.
Enabling the pgcrypto Extension
PostgreSQL has built-in support for UUIDs, but to generate them dynamically inside the database, we use the pgcrypto extension. Think of an extension as an optional plugin that adds specialized functions to your PostgreSQL instance.
Run this command in your psql console or pgAdmin query tool to enable the module:
SQLCREATE EXTENSION IF NOT EXISTS "pgcrypto";
Once enabled, you gain access to gen_random_uuid(), which generates a version 4 (random) UUID.
Implementing UUIDs as Primary Keys
To use a UUID as a Primary key, you must change your column type from INTEGER or SERIAL to UUID.
Let’s update our products table from our store project to use a UUID instead of a standard integer ID.
SQL-- Create a new table using UUID for the primary key CREATE TABLE products ( product_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, price NUMERIC(10, 2) NOT NULL ); -- Insert a record INSERT INTO products (name, price) VALUES ('Mechanical Keyboard', 120.00); -- Verify the result SELECT * FROM products;
When you perform the SELECT, you’ll see a result like a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11. This value is impossible to guess, protecting your application from enumeration.
Hands-on Exercise
- Open your terminal or query tool and ensure the
pgcryptoextension is active. - Create a new table named
customers_securewith two columns:id(typeUUID) andemail(typeTEXT). - Set the
idcolumn toDEFAULT gen_random_uuid(). - Insert three records into the table without providing an ID value.
- Query the table to confirm that PostgreSQL automatically generated unique UUIDs for each row.
Common Pitfalls
- Performance Overhead: UUIDs are 16 bytes, whereas
INTEGERis 4 bytes. While this seems like a large jump, modern hardware handles UUID comparisons extremely efficiently. The security benefits usually outweigh the storage cost. - Readability: UUIDs are difficult to type or communicate verbally. Never use them as "public-facing" URLs if you want them to be human-readable; instead, use the UUID for internal database linking and provide a separate "slug" or "order_number" for users.
- Orderability: Because random UUIDs (v4) are not sequential, they can cause "index fragmentation" on very large tables because the database has to jump around the disk to insert new keys. For most beginner-to-intermediate applications, this is a non-issue.
FAQ
Can I use UUIDs with foreign keys?
Yes. When you set up a relationship between tables, ensure the column referencing the primary key is also defined as UUID.
What is the difference between UUID and SERIAL?
SERIAL is a sequence-based integer (1, 2, 3), while UUID is a random 128-bit identifier. SERIAL is easier to read; UUID is significantly more secure and scalable for distributed systems.
Do I need to store the UUID as a string?
No, always use the dedicated UUID data type. Storing it as TEXT is slower and takes up significantly more space.
Recap
We’ve moved past simple sequences and embraced UUIDs to harden our store schema. By enabling pgcrypto and utilizing gen_random_uuid(), we've ensured our primary keys are globally unique and resistant to enumeration attacks.
Up next: We will cover Handling Timestamps to ensure our store records have accurate, timezone-aware creation dates.
Work with me

VPS Server Setup, Deployment & Hardening
Get your app live on a fast, secure server — properly configured, hardened, and deployment-ready. No more wrestling with the command line.

Laravel SaaS MVP & Multi-Tenant App Development
Launch your SaaS MVP on Laravel — multi-tenant, subscription-ready, and built by the engineer behind a platform serving 10,000+ paying users.


