{"id":31761476,"url":"https://github.com/elitan/postgres-nanoid","last_synced_at":"2025-10-09T21:53:01.073Z","repository":{"id":214055715,"uuid":"735583678","full_name":"elitan/postgres-nanoid","owner":"elitan","description":"Stripe like IDs (e.g., `cus_4fgLw23Dx4fQYd`) in Postgres.","archived":false,"fork":false,"pushed_at":"2025-07-11T08:52:19.000Z","size":51,"stargazers_count":42,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-09-11T16:41:29.503Z","etag":null,"topics":["ids","nanoid","postgres","stripe"],"latest_commit_sha":null,"homepage":"","language":"PLpgSQL","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/elitan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-12-25T12:47:13.000Z","updated_at":"2025-09-07T16:14:19.000Z","dependencies_parsed_at":"2023-12-25T14:16:36.524Z","dependency_job_id":"3e96719e-3334-4ab8-9f39-cd4be27b7342","html_url":"https://github.com/elitan/postgres-nanoid","commit_stats":null,"previous_names":["elitan/pg-nanoid"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/elitan/postgres-nanoid","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elitan%2Fpostgres-nanoid","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elitan%2Fpostgres-nanoid/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elitan%2Fpostgres-nanoid/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elitan%2Fpostgres-nanoid/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/elitan","download_url":"https://codeload.github.com/elitan/postgres-nanoid/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elitan%2Fpostgres-nanoid/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279002044,"owners_count":26083286,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-09T02:00:07.460Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ids","nanoid","postgres","stripe"],"created_at":"2025-10-09T21:52:58.370Z","updated_at":"2025-10-09T21:53:01.068Z","avatar_url":"https://github.com/elitan.png","language":"PLpgSQL","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PostgreSQL Nanoid\n\n**84,000+ IDs/second** • **Secure by default** • **URL-safe** • **Collision-resistant**\n\n❌ Stop using auto-increment IDs that leak your business data.  \n❌ Stop using UUIDs that are ugly, long, and random.  \n✅ Use nanoids: secure, compact, and beautiful.\n\n## ⚡ Try It Now (30 seconds)\n\n```sql\n-- 1. Enable extension\nCREATE EXTENSION IF NOT EXISTS pgcrypto;\n\n-- 2. Install functions (copy from bottom of README)\n\n-- 3. Generate secure, random IDs (recommended default)\nSELECT nanoid('cus_');    -- cus_V1StGXR8_Z5jdHi6B\nSELECT nanoid('ord_');    -- ord_K3JwF9HgNxP2mQrTy  \nSELECT nanoid('user_');   -- user_9LrfQXpAwB3mHkSt\n\n-- 4. Generate sortable IDs (only if you need time ordering)\nSELECT nanoid_sortable('log_');  -- log_0uQzNrIBqK9ayvN1T (⚠️ leaks timing)\n```\n\n## 🎯 Why Nanoids?\n\n| Problem                 | Auto-increment    | UUID          | **Nanoid**       | **Nanoid Sortable** |\n| ----------------------- | ----------------- | ------------- | ---------------- | -------------------- |\n| **Leaks business data** | ❌ Reveals count  | ✅ Secure     | ✅ Secure        | ⚠️ Leaks timing      |\n| **Length**              | ❌ Predictable    | ❌ 36 chars   | ✅ 21 chars      | ✅ 21 chars          |\n| **Sortable by time**    | ⚠️ Single DB only | ❌ Random     | ❌ Random        | ✅ Lexicographic     |\n| **URL-friendly**        | ✅ Yes            | ❌ Has dashes | ✅ Clean         | ✅ Clean             |\n| **Performance**         | ✅ Fast           | ⚠️ Slower     | ✅ Fast          | ✅ Fast              |\n\n**Security recommendation:** Use `nanoid()` by default. Only use `nanoid_sortable()` when time-ordering is essential and you understand the privacy trade-offs.\n\n## 🚀 Performance\n\n```sql\n-- Secure random nanoids (recommended)\nSELECT nanoid('ord_') FROM generate_series(1, 10000);   -- ~85ms = 117,000 IDs/sec\nSELECT nanoid('user_') FROM generate_series(1, 100000); -- ~0.9s = 111,000 IDs/sec\n\n-- Sortable nanoids (use only when needed)\nSELECT nanoid_sortable('log_') FROM generate_series(1, 10000);   -- ~123ms = 81,200 IDs/sec\nSELECT nanoid_sortable('event_') FROM generate_series(1, 100000); -- ~1.18s = 84,700 IDs/sec\n```\n\n**Production ready:**\n\n- ⚡ **110,000+ IDs/second** - random nanoids (fastest, most secure)\n- 🏃 **84,000+ IDs/second** - sortable nanoids (when time-ordering needed)\n- 🔒 **Security-first** - random by default, sortable by choice\n- 💾 **Memory efficient** - streaming generation\n\n## 🎨 Beautiful, Meaningful IDs\n\n```sql\n-- Your old UUIDs\nf47ac10b-58cc-4372-a567-0e02b2c3d479  -- 😵 36 chars, random order\n2514e1ae-3ab3-431e-aa45-225d70d89f61  -- 🤷 Which was created first?\n\n-- Secure random nanoids (recommended)\ncus_V1StGXR8_Z5jdHi6B  -- 😍 21 chars, secure \u0026 random\nord_K3JwF9HgNxP2mQrTy  -- 🔒 No timing information leaked\n\n-- Sortable nanoids (use carefully)\nlog_0uQzNrIBqK9ayvN1T  -- ⏰ 21 chars, time-ordered\nevt_0uQzNrIEg13LGTj4c  -- ⚠️ But reveals creation timing\n```\n\n**When you need time-ordering:**\n\n```sql\n-- Generate sortable IDs over time - naturally sorted!\nWITH events AS (\n    SELECT nanoid_sortable('evt_') as id, pg_sleep(0.001)\n    FROM generate_series(1, 5)\n)\nSELECT id FROM events ORDER BY id;  -- Chronological! (but less secure)\n```\n\n**Security consideration:** Sortable IDs make business activity patterns observable to anyone with access to multiple IDs.\n\n## 🛠️ Production Setup\n\n```sql\n-- Secure table with random nanoid defaults (recommended)\nCREATE TABLE customers (\n    id SERIAL PRIMARY KEY,\n    public_id TEXT NOT NULL UNIQUE DEFAULT nanoid('cus_'),\n    name TEXT NOT NULL\n);\n\n-- Optional: Time-ordered table (use only when chronological sorting is essential)\nCREATE TABLE audit_logs (\n    id SERIAL PRIMARY KEY,\n    event_id TEXT NOT NULL UNIQUE DEFAULT nanoid_sortable('log_'),\n    message TEXT NOT NULL,\n    created_at TIMESTAMP DEFAULT NOW()\n);\n\nINSERT INTO customers (name) VALUES ('Acme Corp'), ('Widget Co');\nSELECT public_id, name FROM customers ORDER BY public_id;\n-- cus_V1StGXR8_Z5jdHi6B | Acme Corp    ← Secure, no timing info\n-- cus_K3JwF9HgNxP2mQrTy | Widget Co    ← Random order preserves privacy\n```\n\n### Production Validation\n\n✅ **Collision resistant** - 2×10¹⁴ years to 1% probability at 1000 IDs/hour  \n✅ **Scale tested** - Millions of records, faster than UUIDs  \n✅ **Index efficient** - Time-ordered = better B-tree locality\n\n## 📖 API\n\n### `nanoid(prefix, size, alphabet, additionalBytesFactor)` - Secure Random (Recommended)\n\n```sql\nSELECT nanoid();                    -- V1StGXR8_Z5jdHi6B-myT\nSELECT nanoid('user_');             -- user_V1StGXR8_Z5jdHi6B\nSELECT nanoid('cus_', 25);          -- cus_V1StGXR8_Z5jdHi6B-my\n```\n\n✅ **Secure:** No timing information  \n✅ **Fast:** ~10% faster than sortable  \n✅ **Private:** Random order preserves business intelligence  \n\n### `nanoid_sortable(prefix, size, alphabet, additionalBytesFactor)` - Time-Ordered\n\n```sql\nSELECT nanoid_sortable();           -- 0uQzNrIBqK9ayvN1T-abc\nSELECT nanoid_sortable('log_');     -- log_0uQzNrIEg13LGTj4c\nSELECT nanoid_sortable('evt_', 25); -- evt_0uQzNrIEutvmf1aS-xy\n```\n\n⚠️ **Security trade-off:** Embeds creation timestamp  \n✅ **Sortable:** Lexicographic time ordering  \n⚠️ **Privacy risk:** Reveals business activity patterns  \n\n### `nanoid_extract_timestamp(nanoid_value, prefix_length)` - Sortable Only\n\n```sql\n-- Extract creation time from sortable nanoids (debugging/analysis)\nSELECT nanoid_extract_timestamp('log_0uQzNrIBqK9ayvN1T', 4);\n-- 2025-07-11 19:13:10.204\n```\n\n## 🚀 Advanced Usage\n\n```sql\n-- Multiple entity types with secure random IDs (recommended)\nSELECT nanoid('cus_');  -- Customer ID (random, secure)\nSELECT nanoid('ord_');  -- Order ID (random, secure)\nSELECT nanoid('inv_');  -- Invoice ID (random, secure)\n\n-- Time-ordered IDs for logs/events (use sparingly)\nSELECT nanoid_sortable('log_');  -- Log entry (sortable, less secure)\nSELECT nanoid_sortable('evt_');  -- Event ID (sortable, less secure)\n\n-- Database constraints\nCREATE TABLE orders (\n    public_id TEXT NOT NULL UNIQUE\n        CHECK (public_id ~ '^ord_[0-9a-zA-Z]{17}$')\n        DEFAULT nanoid('ord_'),  -- Secure random\n    customer_id TEXT CHECK (customer_id ~ '^cus_[0-9a-zA-Z]{17}$')\n);\n\n-- Mixed approach: secure customer IDs, sortable audit trail\nCREATE TABLE user_actions (\n    user_id TEXT CHECK (user_id ~ '^usr_[0-9a-zA-Z]{17}$'), -- Random\n    action_id TEXT DEFAULT nanoid_sortable('act_')           -- Sortable\n);\n\n-- Batch generation\nWITH batch_ids AS (\n    SELECT nanoid('item_') as id, 'Product ' || generate_series as name\n    FROM generate_series(1, 100000)\n)\nINSERT INTO products (public_id, name) SELECT id, name FROM batch_ids;\n-- ~0.9 seconds for 100k secure random IDs\n```\n\n## 🤔 When to Use\n\n### ✅ Use `nanoid()` (secure random) for:\n\n- **Public-facing IDs** (APIs, URLs, customer references)\n- **User-facing identifiers** (account IDs, order numbers)\n- **Multi-tenant applications** (tenant isolation)\n- **Distributed systems** (no coordination needed)\n- **Any case where privacy matters**\n\n### ⚠️ Use `nanoid_sortable()` only when:\n\n- **Temporal ordering is essential** (audit logs, event streams)\n- **You need lexicographic time sorting** (without separate timestamp)\n- **Privacy trade-offs are acceptable** (internal systems only)\n- **Users won't see multiple IDs** (preventing pattern analysis)\n\n### ❌ Consider alternatives for:\n\n- **Internal foreign keys** (integers might be faster)\n- **Legacy system integration** (if systems expect UUIDs)\n- **High-security contexts** (consider longer random IDs)\n\n## 🔧 Installation\n\n### Copy-Paste SQL\n\n\u003cdetails\u003e\n\u003csummary\u003eClick to expand nanoid functions (240+ lines)\u003c/summary\u003e\n\n```sql\nCREATE EXTENSION IF NOT EXISTS pgcrypto;\n\n-- Drop existing functions to ensure clean state\nDROP FUNCTION IF EXISTS nanoid CASCADE;\nDROP FUNCTION IF EXISTS nanoid_sortable CASCADE;\nDROP FUNCTION IF EXISTS nanoid_optimized CASCADE;\nDROP FUNCTION IF EXISTS nanoid_extract_timestamp CASCADE;\n\n-- Create the optimized helper function for random part generation\nCREATE OR REPLACE FUNCTION nanoid_optimized(size int, alphabet text, mask int, step int)\n    RETURNS text\n    LANGUAGE plpgsql\n    VOLATILE LEAKPROOF PARALLEL SAFE\n    AS $$\nDECLARE\n    idBuilder text := '';\n    counter int := 0;\n    bytes bytea;\n    alphabetIndex int;\n    alphabetArray text[];\n    alphabetLength int := 64;\nBEGIN\n    alphabetArray := regexp_split_to_array(alphabet, '');\n    alphabetLength := array_length(alphabetArray, 1);\n    LOOP\n        bytes := gen_random_bytes(step);\n        FOR counter IN 0..step - 1 LOOP\n            alphabetIndex :=(get_byte(bytes, counter) \u0026 mask) + 1;\n            IF alphabetIndex \u003c= alphabetLength THEN\n                idBuilder := idBuilder || alphabetArray[alphabetIndex];\n                IF length(idBuilder) = size THEN\n                    RETURN idBuilder;\n                END IF;\n            END IF;\n        END LOOP;\n    END LOOP;\nEND\n$$;\n\n-- Sortable nanoid function with timestamp encoding (use only if temporal ordering is required)\n-- WARNING: This function embeds timestamps in IDs, which can leak business intelligence\n-- and timing information. Use the regular nanoid() function for better security.\nCREATE OR REPLACE FUNCTION nanoid_sortable(\n    prefix text DEFAULT '', \n    size int DEFAULT 21, \n    alphabet text DEFAULT '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', \n    additionalBytesFactor float DEFAULT 1.02\n)\n    RETURNS text\n    LANGUAGE plpgsql\n    VOLATILE LEAKPROOF PARALLEL SAFE\n    AS $$\nDECLARE\n    timestamp_ms bigint;\n    timestamp_encoded text := '';\n    remainder int;\n    temp_ts bigint;\n    random_size int;\n    random_part text;\n    finalId text;\n    alphabetArray text[];\n    alphabetLength int;\n    mask int;\n    step int;\nBEGIN\n    -- Input validation\n    IF size IS NULL OR size \u003c 1 THEN\n        RAISE EXCEPTION 'The size must be defined and greater than 0!';\n    END IF;\n    IF alphabet IS NULL OR length(alphabet) = 0 OR length(alphabet) \u003e 255 THEN\n        RAISE EXCEPTION 'The alphabet can''t be undefined, zero or bigger than 255 symbols!';\n    END IF;\n    IF additionalBytesFactor IS NULL OR additionalBytesFactor \u003c 1 THEN\n        RAISE EXCEPTION 'The additional bytes factor can''t be less than 1!';\n    END IF;\n    \n    -- Get current timestamp and encode using nanoid alphabet (inline for simplicity)\n    timestamp_ms := extract(epoch from clock_timestamp()) * 1000;\n    alphabetArray := regexp_split_to_array(alphabet, '');\n    alphabetLength := array_length(alphabetArray, 1);\n    temp_ts := timestamp_ms;\n    \n    -- Handle zero case\n    IF temp_ts = 0 THEN\n        timestamp_encoded := alphabetArray[1];\n    ELSE\n        -- Convert to base using nanoid alphabet\n        WHILE temp_ts \u003e 0 LOOP\n            remainder := temp_ts % alphabetLength;\n            timestamp_encoded := alphabetArray[remainder + 1] || timestamp_encoded;\n            temp_ts := temp_ts / alphabetLength;\n        END LOOP;\n    END IF;\n    \n    -- Pad to 8 characters for consistent lexicographic sorting\n    WHILE length(timestamp_encoded) \u003c 8 LOOP\n        timestamp_encoded := alphabetArray[1] || timestamp_encoded;\n    END LOOP;\n    \n    -- Calculate remaining size for random part  \n    random_size := size - length(prefix) - 8; -- 8 = timestamp length\n    \n    IF random_size \u003c 1 THEN\n        RAISE EXCEPTION 'The size including prefix and timestamp must leave room for random component! Need at least % characters.', length(prefix) + 9;\n    END IF;\n    \n    -- Generate random part using optimized function\n    mask := (2 \u003c\u003c cast(floor(log(alphabetLength - 1) / log(2)) AS int)) - 1;\n    step := cast(ceil(additionalBytesFactor * mask * random_size / alphabetLength) AS int);\n    \n    IF step \u003e 1024 THEN\n        step := 1024;\n    END IF;\n    \n    random_part := nanoid_optimized(random_size, alphabet, mask, step);\n    \n    -- Combine: prefix + timestamp + random\n    finalId := prefix || timestamp_encoded || random_part;\n    \n    RETURN finalId;\nEND\n$$;\n\n-- Main nanoid function - purely random, secure by default\nCREATE OR REPLACE FUNCTION nanoid(\n    prefix text DEFAULT '', \n    size int DEFAULT 21, \n    alphabet text DEFAULT '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', \n    additionalBytesFactor float DEFAULT 1.02\n)\n    RETURNS text\n    LANGUAGE plpgsql\n    VOLATILE LEAKPROOF PARALLEL SAFE\n    AS $$\nDECLARE\n    random_size int;\n    random_part text;\n    finalId text;\n    alphabetLength int;\n    mask int;\n    step int;\nBEGIN\n    -- Input validation\n    IF size IS NULL OR size \u003c 1 THEN\n        RAISE EXCEPTION 'The size must be defined and greater than 0!';\n    END IF;\n    IF alphabet IS NULL OR length(alphabet) = 0 OR length(alphabet) \u003e 255 THEN\n        RAISE EXCEPTION 'The alphabet can''t be undefined, zero or bigger than 255 symbols!';\n    END IF;\n    IF additionalBytesFactor IS NULL OR additionalBytesFactor \u003c 1 THEN\n        RAISE EXCEPTION 'The additional bytes factor can''t be less than 1!';\n    END IF;\n    \n    -- Calculate random part size (full size minus prefix)\n    random_size := size - length(prefix);\n    \n    IF random_size \u003c 1 THEN\n        RAISE EXCEPTION 'The size must be larger than the prefix length! Need at least % characters.', length(prefix) + 1;\n    END IF;\n    \n    alphabetLength := length(alphabet);\n    \n    -- Generate purely random part using optimized function\n    mask := (2 \u003c\u003c cast(floor(log(alphabetLength - 1) / log(2)) AS int)) - 1;\n    step := cast(ceil(additionalBytesFactor * mask * random_size / alphabetLength) AS int);\n    \n    IF step \u003e 1024 THEN\n        step := 1024;\n    END IF;\n    \n    random_part := nanoid_optimized(random_size, alphabet, mask, step);\n    \n    -- Combine: prefix + random (no timestamp)\n    finalId := prefix || random_part;\n    \n    RETURN finalId;\nEND\n$$;\n\n-- Helper function to extract timestamp from sortable nanoid (only works with nanoid_sortable)\nCREATE OR REPLACE FUNCTION nanoid_extract_timestamp(\n    nanoid_value text, \n    prefix_length int DEFAULT 0,\n    alphabet text DEFAULT '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'\n)\n    RETURNS timestamp\n    LANGUAGE plpgsql\n    IMMUTABLE LEAKPROOF PARALLEL SAFE\n    AS $$\nDECLARE\n    timestamp_encoded text;\n    timestamp_ms bigint := 0;\n    alphabetArray text[];\n    alphabetLength int;\n    char_pos int;\n    i int;\nBEGIN\n    -- Extract 8-character timestamp after the prefix\n    timestamp_encoded := substring(nanoid_value, prefix_length + 1, 8);\n    alphabetArray := regexp_split_to_array(alphabet, '');\n    alphabetLength := array_length(alphabetArray, 1);\n    \n    -- Decode from base using nanoid alphabet (inline for simplicity)\n    FOR i IN 1..length(timestamp_encoded) LOOP\n        char_pos := array_position(alphabetArray, substring(timestamp_encoded, i, 1));\n        IF char_pos IS NULL THEN\n            RAISE EXCEPTION 'Invalid character in timestamp: %', substring(timestamp_encoded, i, 1);\n        END IF;\n        timestamp_ms := timestamp_ms * alphabetLength + (char_pos - 1);\n    END LOOP;\n    \n    -- Convert to timestamp\n    RETURN to_timestamp(timestamp_ms / 1000.0);\nEXCEPTION\n    WHEN OTHERS THEN\n        RAISE EXCEPTION 'Invalid nanoid format or timestamp extraction failed: %', SQLERRM;\nEND\n$$;\n```\n\n\u003c/details\u003e\n\n### Development Environment\n\n```bash\n# Clone and test with Docker\ngit clone https://github.com/your-repo/postgres-nanoid\ncd postgres-nanoid\nmake up \u0026\u0026 make test-all  # Start + test everything\nmake psql                 # Connect and try it\n```\n\n---\n\n**Made your IDs better?** Give us a ⭐ on GitHub!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felitan%2Fpostgres-nanoid","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Felitan%2Fpostgres-nanoid","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felitan%2Fpostgres-nanoid/lists"}