CVE-2026-26007: Subgroup Confinement Attack in pyca/cryptography

A single missing validation check in pyca/cryptography recently exposed how legacy elliptic curve math can quietly leak private keys.

If you run a Python backend, pyca/cryptography likely forms the core of your security posture. On February 10, 2026, the maintainers released version 46.0.5 to address a high-severity vulnerability tracked as CVE-2026-26007 (GHSA-r6ph-v2qm-q3c2) with a CVSS score of 8.2. This is a textbook cryptographic subgroup confinement attack that allows an attacker to leak private key bits during Elliptic Curve Diffie-Hellman (ECDH) key exchanges or forge signatures in ECDSA. Understanding this flaw requires a deep dive into abstract algebra and legacy cryptographic curves.

The Mathematics of Subgroup Confinement

Elliptic curve cryptography operates over finite fields where curve points form an abelian group. Secure implementations work within a subgroup of a large prime order n. The relationship between the total group order N and the prime subgroup order n is defined by the cofactor h, where N = h * n.

In modern curves like NIST P-256 or secp256k1, the cofactor is exactly 1. The total group order is prime, making them immune to subgroup attacks. However, legacy binary curves, often called SECT curves like SECT163K1 or SECT283R1, have cofactors greater than 1, typically 2 or 4.

When the cofactor exceeds 1, the group contains small-order subgroups. If an application performing an ECDH key exchange does not validate that the incoming public key point lies within the correct subgroup, an attacker can exploit this by transmitting a malicious public key point P belonging to a small-order subgroup of order r.

When the victim computes the shared secret S = d * P, where d is the private key, the resulting point S falls within that subgroup. Since there are only r possible values for S, the attacker can deduce the value of d modulo r by observing subsequent encrypted communication. Repeating this with different subgroups allows an attacker to use the Chinese Remainder Theorem to reconstruct the private key.

The Vulnerability in pyca/cryptography

The flaw in pyca/cryptography versions up to 46.0.4 stems from a lack of strict validation when parsing public keys. When an application loaded an elliptic curve public key from DER or PEM formats, or constructed it using public_key_from_numbers(), the library did not verify whether the point belonged to the proper prime-order subgroup.

For curves with a cofactor of 1, this omission is harmless. But for binary SECT curves, this allowed maliciously crafted public keys to pass through the loading phase undetected, exposing downstream ECDH or ECDSA operations to key-leakage attacks.

Analyzing the Rust Patches

To fix this, the maintainers modified the Rust-based backend in version 46.0.5. Since pyca/cryptography leverages Rust for performance, the validation logic belongs in the native layer wrapping OpenSSL.

Here is the validation implemented in the Rust layer of the codebase:

impl ECPublicKey {
    fn new(
        pkey: openssl::pkey::PKey<openssl::pkey::Public>,
        curve: pyo3::Py<pyo3::PyAny>,
    ) -> CryptographyResult<ECPublicKey> {
        let ec = pkey.ec_key()?;
        check_key_infinity(&ec)?;

        let mut bn_ctx = openssl::bn::BigNumContext::new()?;
        let mut cofactor = openssl::bn::BigNum::new()?;

        ec.group().cofactor(&mut cofactor, &mut bn_ctx)?;
        let one = openssl::bn::BigNum::from_u32(1)?;

        if cofactor != one {
            ec.check_key().map_err(|_| {
                pyo3::exceptions::PyValueError::new_err(
                    "Invalid EC key: point does not belong to the correct subgroup",
                )
            })?;
        }

        Ok(ECPublicKey { pkey, curve })
    }
}

This fix is highly optimized. For modern curves with a cofactor of 1, the validation check is bypassed. If a legacy curve is detected, it triggers OpenSSL’s internal key check to validate that the point respects the mathematical bounds of the prime-order subgroup.

Simultaneously, the maintainers deprecated all SECT curves in the Python layer, scheduling them for complete removal in version 47.0.0.

The Architectural Takeaway: Keep It Simple

This vulnerability highlights a critical security engineering principle: complexity is the enemy of security. Legacy curves were designed when saving CPU cycles was worth the mathematical complexity of non-prime cofactors. Today, those optimizations are obsolete.

When we designed VaultKeepR for secure credential storage, we avoided this entire class of cryptographic vulnerability by eliminating legacy asymmetric protocols. With VaultKeepR, we minimized runtime validation risks by relying on a strict, modern cryptographic profile that completely bypasses legacy curves. Instead of handling complex on-the-fly elliptic curve negotiations on central servers, we utilize on-device cryptography using modern symmetric primitives.

By relying on Argon2id for key derivation and XChaCha20-Poly1305 for symmetric encryption, we ensure that there are no complex mathematical subgroup vulnerabilities to exploit. All operations happen client-side before sync.

If you maintain legacy systems relying on binary curves, migrate immediately. Transition your workloads to modern curves like Ed25519 and pin your Python environments to cryptography>=46.0.5.

The lifecycle of cryptographic primitives always bends toward simplicity. As older, more complex structures reveal their vulnerabilities under modern analysis, the industry must pivot toward designs where security is mathematically guaranteed by default, rather than enforced by complex runtime checks.

The One Thing I See in Every Developer Who Gets Unstuck

I’ve mentored over 100 developers. Some were juniors. Some had 5 years of experience but felt stuck.

And the one thing I see in every single one of them?

They didn’t need more tutorials. They needed someone to say, “You’re not alone.“

When you’re learning alone, every bug feels like failure. Every error becomes a reason to quit.

But when you’re in a community, that same bug becomes a conversation. You ask, someone answers, you fix it, you move on.

That’s the difference between surviving and thriving.

What I’m building:
A community where developers build real things – together. Not a “watch and repeat” course. A live, project-based, collaboration-first learning space.

Why you should join:
You’ll build projects that actually work
You’ll get feedback from me and other builders
You’ll stop feeling alone in your learning journey

I’m launching this in a few weeks. If you’re a developer who wants to build, not just watch – follow me.

My portfolio (proof that I build what I teach): My Portfolio

What’s one thing you’d love to learn but feel too intimidated to start alone?

Under the Hood: Building an Interactive 1,536-Dimensional Vector Space Visualizer with React & PCA

Every developer working with Large Language Models quickly learns about vector embeddings—arrays of floating-point numbers mapping words, sentences, or images into multi-thousand-dimensional semantic spaces. But while we write APIs calling text-embedding-3-small daily, humans lack the biological architecture to conceptualize 1536-dimensional coordinates.

To bridge this intuitive void, we built Vector Space Explorer: an interactive web visualizer allowing developers to input custom vocabularies, perform real vector arithmetic (like puppy - dog + cat = kitten), play semantic clustering games, and examine the raw JSON outputs returned from deep learning hubs.

Here is the technical architectural breakdown of how we built this application using React 18, Tailwind CSS, and pure client-side linear algebra math.

1. Multi-Provider Endpoint Ingestion

Depending on budget or privacy restrictions, developers use different pipelines. To serve all needs, we wrapped our request adapters to handle multiple standard API specifications through a unified, client-secured interface:

  1. Simulated (Mock Mode): An offline, high-speed, lightweight client-side embedder calculating mathematical coordinates internally so developers can test layouts instantaneously without keys.
  2. OpenAI Cloud: Requests fetched from the secure gateway using text-embedding-3-small (1,536-dim).
  3. LM Studio (Local): Allows local offline execution of state-of-the-art open models like nomic-embed-text-v1.5 on port 1234.
  4. Featherless.ai & OpenRouter: Direct serverless endpoints mapping standard OpenAI-compatible JSON responses.

Here is how the API layer executes the ingestion of raw words:

// excerpt from src/utils/api.ts
export async function fetchWordEmbedding(
  word: string, 
  settings: SystemSettings
): Promise<number[]> {
  if (settings.demoMode) {
    return generateMockEmbedding(word); // Instant client-side PCA-friendly vector
  }

  const response = await fetch(`${settings.baseUrl}/embeddings`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${settings.apiKey}`
    },
    body: JSON.stringify({
      input: word,
      model: settings.model
    })
  });

  if (!response.ok) {
    throw new Error(`Endpoint status code error: ${response.status}`);
  }

  const json = await response.json();
  return json.data[0].embedding; // Standard 1,536 float elements
}

2. The Math Behind the Visualization: Client-Side PCA

Fetching a 1,536-dimensional array is only the beginning. To render this on a computer screen, we must flatten 1,536 axes down to just 2 dimensions ($X, Y$) while preserving as much cluster structure and relative similarity as possible.

While we could send data back to Python for Scikit-Learn’s PCA, doing so ruins UI snappiness. We solved this by writing a pure TypeScript client-side Principal Component Analysis engine utilizing raw Matrix math:

Step 2.1: Constructing the Covariance Matrix

First, we mean-center the coordinate matrices of all words currently added to the sandbox, then calculate their covariance. This maps the directional associations of our coordinates.

Sigma = frac{1}{n} X^T X

Step 2.2: SVD via Jacobi Eigenvalue Algorithm

To extract the two main components (the directions capturing the highest amount of variance), we must extract the eigenvectors from our covariance matrix. We write a iterative Jacobi sweep solver to diagonalize symmetric matrices direct in TypeScript:

// Clean conceptual loop of Jacobi Eigenvalue Solver
export function solveEigenvectors(covariance: number[][], maxSweeps = 50) {
  const size = covariance.length;
  const eigenvectors = createIdentityMatrix(size);
  let matrix = cloneMatrix(covariance);

  for (let sweep = 0; sweep < maxSweeps; sweep++) {
    let offDiagonalSum = computeOffDiagonalNorm(matrix);
    if (offDiagonalSum < 1e-9) break; // Diagonalized successfully!

    for (let p = 0; p < size; p++) {
      for (let q = p + 1; q < size; q++) {
        const theta = calculateRotationAngle(matrix, p, q);
        const [c, s] = [Math.cos(theta), Math.sin(theta)];

        matrix = applyJacobiRotation(matrix, p, q, c, s);
        updateEigenvectorSet(eigenvectors, p, q, c, s);
      }
    }
  }
  return { eigenvalues: extractDiagonals(matrix), eigenvectors };
}

Step 2.3: Projecting down to 2D

We sort computed eigenvalues descending, choose the eigenvectors corresponding to the top two eigenvalues, and project our high-dimensional vectors onto those top two Principal Components to compute coordinates on our responsive galaxy map.

3. Why are “puppy” and “dog” far apart visually?

A common question emerges when users construct clusters in Sandbox mode:

“If puppy and dog have a Cosine Similarity of 0.85 (extremely high), why are they rendered far apart on the 2D constellation grid?”

This paradox illustrates the exact mathematical limitation of dimensional projection.

    ┌──────────────────────────┐
    │  1,536-Dimensional Space │ ➔ (true relationship is extremely adjacent)
    └─────────────┬────────────┘
                  │ PCA Lossy Compress
                  ▼
    ┌──────────────────────────┐
    │     2D Screen Canvas     │ ➔ (compressed projection can distort angles)
    └──────────────────────────┘
  1. Loss of Variance Info: Standard embeddings span 1,536 dimensions representation. High-similarity dimensions might point along eigenvectors #12 or #24 which are entirely discarded in order to squeeze the map onto Component #1 and #2.
  2. Global Optimization Context: PCA determines its coordinate calculations based exclusively on the currently visible set of sandbox stars. If you only have “cat”, “dog”, and “puppy” in the sandbox, the eigenvalues will polarize. By inputting highly distinct vocabulary structures (e.g., adding “planet”, “computer”, “compiler”, and “jupiter”), PCA gains rich context nodes, pulling “dog”, “puppy”, and “cat” close together into a dense animal sub-cluster while pushing tech words across to the opposite quadrant!

To offset this limitation, our side Telemetry Panel calculates both the 2D Euclidean offset and the True High-Dimensional API Cosine Similarity as you select nodes, teaching developers how to interpret projection skew.

4. Deep Vector Algebra Calculations

One of the application’s proudest features is its interactive Algebra Lab, which allows executing vector math directly in the browser.

Executing vector("puppy") - vector("dog") + vector("cat") produces a new 1,536-dimensional target coordinate array. We search all active sandbox star arrays to identify the absolute closest semantic neighbor utilizing Cosine Vector angles:

$$
text{Similarity}(A, B) = frac{A cdot B}{|A| |B|}
$$

The model traces an animated emerald trajectory line showing how close the synthesized conceptual vector came to landing exactly on its semantic targets (like #kitten).

By maintaining robust client state, loading dynamic SVG/Canvas layers smoothly, and managing API key parameters privately in browser memory without server tracking, the application guarantees secure, stateless, and incredibly educational sandbox interactions.

Fork the codebase on GitHub to explore adding 3D orbital environments, cluster mapping, or real-time Vector DB indexing visuals today!

Code and more: https://www.dailybuild.xyz/project/139-vector-space-explorer

The $1000 Operating System: Rearchitecting the Dev Team Inside Antigravity 2.0

This is a submission for the Google I/O Writing Challenge

Earlier this week, a software system built the core framework of a functioning operating system from scratch in exactly 12 hours. No human wrote a single line of code. The architecture wasn’t planned by a principal engineer; it was broken down, assigned, and compiled entirely by an autonomous ecosystem of AI agents.

The entire project cost less than $1000 in API compute credits.

If your current relationship with generative AI consists of manually treating a language model like a highly advanced autocomplete tool—typing a prompt, waiting for an isolated snippet of syntax, and copy-pasting it into your IDE—you are using a paradigm that Google just retired. At Google I/O 2026, the company shifted its focus entirely away from passive chatbot extensions. We are entering the era of independent, multi-agent orchestration engines.

As a Google Developer Expert (GDE) who designs and breaks these frameworks daily, I can tell you that this isn’t an incremental tooling upgrade. It changes the fundamental unit economics of what is financially and structurally viable to build.

1. Inside the 12-Hour Build: Parallel Orchestration in Action

During the developer keynote, Google DeepMind’s Varun Mohan introduced Antigravity 2.0, which has transitioned from an experimental IDE helper into a standalone, agent-first desktop application. To make the engineering capabilities of the platform clear, Mohan dropped the telemetry of their internal stress test: a 93-agent team assigned to write an OS.

  • The Team Size: 93 autonomous sub-agents spun up on demand.
  • The Compute Load: 15000 API requests processing 2.6 billion tokens.
  • The Operational Cost: Under $1000 USD using the new Gemini 3.5 Flash engine.

Instead of stretching one massive context window to its absolute breaking point with a single giant prompt, the core Antigravity agent acted strictly as an engineering manager. It broke the master task into parallel micro-objectives, spinning off sub-agents to handle isolated modules independently. One group engineered the memory management sub-systems, another excelled at the process scheduler, and separate agents concurrently generated and executed unit tests within an isolated containerized sandbox.

The GDE Take: What fascinated me about this run wasn’t just that the code compiled without human syntax correction; it was the ecosystem’s capacity for autonomous problem-solving. When Mohan’s team first attempted to run the classic game DOOM on the newly compiled OS onstage, the game stalled due to missing video and keyboard drivers. I’m currently testing with some agents in current version in Gemini (only Agent Manager was updated so far).

Instead of a developer jumping in to write a wrapper, Mohan simply gave the agent harness a high-level correction command. The main agent automatically recognized the missing dependencies, deployed a specialized driver sub-agent, wrote the code, ran a quick compilation test, and launched the game live on stage. As an architect, seeing a multi-agent system patch its own environment mid-execution changes my entire reference point for what “agentic coding” means.

2. Fueling the Token Monster: Why Silicon Architecture Matters

If you manage application infrastructure or keep a tight eye on cloud spend, looking at a 15,000-request architecture probably gives you financial anxiety. Running nearly a hundred agents concurrently to process billions of tokens is a massive “token monster.” If you try to run this kind of dense, recurring automation pattern over traditional, generalized infrastructure models, your operational budgets will disappear before mid-year.

To understand why this is suddenly cost-effective, we have to look at how Google co-optimized the model layer with their hardware fabric:

  • Gemini 3.5 Flash: This model is designed directly for the low-latency, rapid-fire iterations required by agent loops. Inside the Antigravity system harness, Flash is co-optimized to run up to 12x faster than standard frontier models, intentionally prioritizing execution throughput and cost efficiency without dropping the semantic reasoning necessary to maintain systemic code architecture.
  • TPU v8i Inference Chips: Standard AI hardware is typically built for massive, heavy-duty training workloads—the computing equivalent of a cargo train. For Antigravity 2.0, Google split its 8th-generation silicon footprint, creating the TPU v8i variant optimized strictly for real-time inference. It handles massive, parallelized token streams without letting the agent loops stall while waiting for compute availability.
  • Jackson Pathways: Imagine all 93 sub-agents blasting billions of tokens back and forth within a single local server rack; the localized bottleneck would completely freeze execution. Jackson Pathways acts as a dynamic traffic routing network, seamlessly distributing token loads across globally separate data center clusters in real time, keeping the entire pipeline completely fluid.

3. The Real Frontier: Autonomy Demands Absolute Responsibility

The system engineering behind Antigravity is a massive technical win, but the most important part of the keynote didn’t involve code telemetry. It came during Demis Hassabis’ final segment, where he explicitly noted that we are standing in the “foothills of the singularity.” For developers entering this space, we need a clear, unhyped understanding of what that actually implies. When an expert like Hassabis references the “singularity,” he isn’t talking about science fiction tropes or self-aware machines; he is describing a highly specific technological inflection point where AI systems become advanced enough to recursively design, iterate, and optimize software entirely independent of human engineers.

When code can propagate, scale, and compile itself across 93 parallel threads, human code review pipelines become a massive operational bottleneck. Our roles are fundamentally shifting from syntax writers to telemetry auditors, security gatekeepers, and boundary architects. If you let teams of autonomous sub-agents run wild in a repository without explicit constraints, security vulnerabilities and architectural debt will multiply faster than your team can track them. As architects, we have to master the defensive guardrails Google outlined:

Automated Pipeline Defense via Code Mender

We can no longer treat security as a post-build chore. Google highlighted Code Mender, an automated defense agent built directly into the integration layer. As your Antigravity sub-agents generate and push code to your sandbox, Code Mender runs alongside them asynchronously, scanning code blocks for critical vulnerabilities and committing automated security patches before the software ever touches a production branch.

Immutable Data Provenance (SynthID & C2PA)

In a development landscape flooded with autonomous agent systems, knowing the exact lineage of your code and media assets is a non-negotiable requirement for system compliance. Google announced widespread, unified backing for its SynthID watermarking engine and C2PA Content Credentials alongside industry majors like OpenAI, Microsoft, Nvidia, and ElevenLabs. This infrastructure creates a permanent, tamper-proof cryptographic paper trail. For a developer, this means you can cryptographically verify exactly which agent system generated a file, what data model it trained on, and whether a code block has been altered post-compilation.

4. Conclusion: Moving Up the Stack

The shift from single-turn prompts to autonomous orchestration frameworks marks the graduation of our industry. The core developer discipline of the next decade won’t be defined by how efficiently we look up documentation or format language blocks inside an IDE interface. The future belongs to the system architects—the engineers who know how to design multi-agent communication networks, configure secure containerized sandboxes, and orchestrate automated guardrails around independent code generation layers.

We aren’t being replaced; we are being promoted. The baseline engineering execution is becoming a commodity, forcing us to focus entirely on intent, boundaries, and overall system logic. The question is no longer about how to write a specific software utility—it’s about what system architecture you are ready to let a coordinated ecosystem of agents build for you.

Over to the community: If you had a $1000 token budget to spin up a 90-agent team inside Antigravity 2.0 right now, what manual deployment pipeline or complex architecture component are you letting them automate first? Let’s talk about system design in the comments below!

I replaced a $50/month OCR API with Gemma 4’s native vision (4B model, local, free). Here’s the exact script + preprocessing trick. #gemma #google

stephen_sebastian_c85ea2b profile

AI Agent Memory: Build vs Buy for Enterprise Teams

Every AI team eventually hits this question

Your agents need persistent memory. That’s settled. The question engineering leaders are now asking is: do we build the memory infrastructure ourselves, or buy a managed solution?

This is not a simple question. The answer changes dramatically based on your team size, compliance posture, and time-to-market pressure. This post gives you the honest framework to make that call — not the answer designed to sell you something.

(Full disclosure: we build Trace Continuity. We’ll tell you when building makes more sense.)

The problem: AI memory without governance is a liability

Before the build vs. buy decision, there’s a framing decision that most teams get wrong.

The question is not “do we need AI memory?” You do. The question is “do we need governed AI memory?”

In a regulated environment, the answer is yes — and governed memory is meaningfully harder to build than plain memory.

Here’s what “governed AI memory” actually requires in production:

  • PII auto-redaction before anything reaches storage
  • Retention policies enforced at the infrastructure layer
  • Immutable audit logs for every read, write, and delete
  • Multi-tenant isolation enforced architecturally
  • Deletion workflows with proof-of-deletion for GDPR Article 17 and CCPA compliance
  • Access control scoped per memory, per agent role

If your AI agents touch patient data, financial records, legal documents, or employee information — that entire list is required.

The build path: what it actually costs

The minimum viable memory layer (2-4 weeks)

A basic memory layer — embed, store, retrieve — is genuinely not that hard. A vector store (pgvector, Pinecone, Weaviate), an embedding pipeline, a retrieval API. An experienced engineer can have this running in two weeks.

This is the part teams budget for. It’s not the expensive part.

Adding governance (3-6 months)

Once the basic layer works, the questions start arriving:

  • “How do we enforce data retention? HIPAA says we can’t hold PHI longer than clinically necessary.”
  • “Which agents can access which memories?”
  • “Our compliance team needs an audit log.”
  • “A user exercised GDPR right to erasure. Can we prove we deleted everything?”
  • “PII is leaking into the vector store.”

Each of those is a separate engineering project. Realistically: a team of 2-3 engineers, 6-12 months, before you have something you’d put in front of an auditor.

Ongoing maintenance burden

The build cost is not one-time. Governance infrastructure requires:

  • Staying current on regulatory changes
  • Responding to security incidents and CVEs
  • Building tooling for compliance reporting
  • Supporting deletion workflows

The buy path: what a managed solution actually provides

Governance as infrastructure, not application code

With a managed solution like Trace Continuity, the governance layer is not something your developers implement on top of the memory store. It is the memory store.

// Every write passes through: PII scan -> redact -> TTL-enforce -> access-control -> audit-log
await memory.remember({
  agent: "intake-bot",
  tenant: "acme-corp",
  fact: "Patient prefers morning appointments. DOB: 1978-04-15.",
  retention: "365d",
  access: ["clinical-ops"]
});
// Stored: "Patient prefers morning appointments. DOB: [REDACTED]."
// Redaction event logged. TTL set. Access policy stored. Audit record created.

What “managed” means for compliance

Requirement Build-it-yourself Managed solution
PII redaction You build detection pipeline Pre-storage, 15+ PII types, audit log
Retention enforcement Cron jobs, your logic, your bugs Infrastructure-layer TTL, automatic
Audit logs You design the schema and queries Queryable by agent/tenant/time, exportable
GDPR deletion proof Manual workflow, hope it works forget() with immutable proof of deletion
Multi-tenant isolation Namespace conventions, developer discipline Architectural enforcement, 403 on mismatch
Access control API key scoping Per-memory, per-agent-role policies

Compliance certifications you don’t have to earn

SOC 2 Type II and HIPAA BAA are table stakes for enterprise sales. Earning SOC 2 Type II in-house requires 6-12 months of audit preparation. A managed solution transfers that burden.

The decision framework

Build if:

  • Your compliance requirements are zero or negligible
  • You have a genuinely differentiated memory architecture
  • Your team has available engineering capacity and a long runway (3-5 engineers, 12+ months)

Buy if:

  • You’re in a regulated industry (healthcare, fintech, HR tech, legal, insurance)
  • Enterprise deals require compliance documentation
  • Time-to-market is a constraint
  • You’re a startup or growth-stage company

The mistake most teams make

Teams underscope the build. They plan for the vector store and the retrieval API — the 2-4 week project. Then governance lands on the roadmap mid-build and pushes the delivery date by 6 months.

If you’re going to build, scope the governance from day one.

If you’re going to buy, buy early. The cost of running ungoverned memory in a regulated environment while the build project runs over deadline is not just engineering time. It’s liability.

What Trace Continuity provides

Trace Continuity is governed AI memory infrastructure for teams that need to move fast without accumulating compliance debt.

  • REST API for writing, reading, and governing agent memory
  • PII auto-redaction before storage, 15+ types out of the box
  • Retention policies enforced at the infrastructure layer
  • Immutable audit logs for every memory operation
  • Multi-tenant isolation enforced architecturally
  • GDPR/CCPA-compatible deletion workflows with proof

Free tier available. No credit card required.

Read the API documentation
See pricing

Further reading

2026 LG OLED TV Serisi Ön Siparişe Açıldı

LG Electronics, CES 2026’da tanıttığı yeni nesil OLED TV serisini Türkiye’de ön siparişe açtı. OLED kategorisindeki 13 yıllık uzmanlığını yeni nesil OLED evo teknolojileriyle ileri taşıyan LG; gelişmiş görüntü performansı, yapay zekâ destekli kullanıcı deneyimi ve premium tasarım yaklaşımıyla dikkat çeken 2026 OLED TV ailesini lansmana özel avantajlarla kullanıcılarla buluşturuyor. Yeni nesil LG OLED evo modelleri; daha yüksek parlaklık seviyesi, gelişmiş renk doğruluğu ve derin kontrast performansıyla daha etkileyici bir izleme deneyimi sunuyor. Hyper Radiant Color Technology ve yeni nesil Alpha 11 AI işlemci sayesinde içerikler; sahneye, ortam ışığına ve kullanım alışkanlıklarına göre gerçek zamanlı optimize ediliyor. Gelişmiş yapay zekâ mimarisi ise görüntü netliği, doku koruması ve ses performansını birlikte değerlendirerek daha doğal ve sürükleyici bir deneyim oluşturuyor. Yüksek yenileme hızları, ultra düşük tepki süresi ve gelişmiş oyun özellikleriyle 2026 LG OLED serisi; sinema, spor ve oyun deneyimini bir üst seviyeye taşıyor. Kendi kendine ışık yayan piksel teknolojisiyle OLED kategorisinin […]

2026 LG OLED TV Serisi Ön Siparişe Açıldı yazısı ilk olarak Teknoloji haberleri – LOG üzerinde yayınlandı.

AI Memory Governance for Legal Tech: How Contract AI Agents Handle Privileged Data

The problem: legal AI agents process data that cannot be mixed

A litigation firm deploys an AI agent to help associates review discovery documents. The agent needs to remember which documents have been analyzed, which privilege log decisions were made, and what matters still need review. This is a legitimate use case — the agent should build context across sessions.

But the discovery documents contain privileged communications between attorneys and clients. When the same AI agent is deployed for a different matter, it must not retain any memory of the first matter. And when the client requests their file in discovery, every access to their data — including AI memory retrieval — must be logged in a way that survives attorney-client privilege scrutiny.

This is the core tension in legal AI memory: the same properties that make AI memory useful (persistent, cross-session, context-building) are the properties that create privilege exposure and compliance risk.

Contract AI agents (contract review, redline comparison, obligation tracking) face similar constraints. A contract agent working on multiple M&A deals cannot remember deal terms from Deal A when working on Deal B. An IP due diligence agent reviewing patent portfolios for Buyer A cannot surface knowledge from Buyer B’s portfolio.

Why generic memory stores fail for legal tech

No multi-tenant isolation at the data layer

Standard memory stores treat all memories as equivalent. For a law firm running a single AI agent across multiple client matters, this means:

  • Matter A’s strategy discussion surfaces in Matter B’s context — privilege crossover
  • Client C’s document review history is available to the agent when it switches to Client D’s matter — conflict of interest
  • A privilege log decision made in Matter A can unconsciously influence the agent’s analysis in Matter B

Multi-tenant isolation at the query level is not enough. The memory store must architecturally separate data at the tenant + matter scope.

No privilege boundary enforcement

Most memory systems have no concept of privileged vs. non-privileged access. Legal AI memory needs:

  • PII detection — client names, matter numbers, document IDs tokenized before storage
  • Deterministic matching — the same client or matter identifier always produces the same token
  • Audit logging — every detection event logged with PII type, token prefix, and timestamp

No discovery-ready audit trail

When opposing counsel requests production of AI system logs in litigation, most memory solutions cannot answer basic questions: What did the AI agent see? When did it access it? Who authorized that access?

How governed memory solves legal AI data challenges

Matter-level isolation with no crossover

When a legal AI agent writes a memory, the memory is scoped to a matter identifier. Matter A’s memories are only accessible when the agent is actively operating in Matter A’s context.

// Write memory scoped to a specific legal matter
const response = await fetch("https://tracecontinuity.com/v1/memories", {
  method: "POST",
  headers: {
    "Authorization": "Bearer mnm_your_api_key",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    agent: "m-and-a-contract-review",
    content: "Matter ACME-2024-089: Anti-sandbagging clause identified in Section 8.3. Target counsel flagged as resistant to MAC clause.",
    retention: "180d",
    scope: "matter:ACME-2024-089"
  })
});
// In a different matter session — ACME-2024-089 memories are not retrieved
// This is architecturally enforced — not a convention

Deterministic tokenization for attorney-client privilege

Documents reviewed by legal AI agents often contain client names, case references, attorney work product, and privileged communications.

const crypto = require("crypto");
function tokenizeMatterId(value, secretKey) {
  const normalized = value.toString().trim().toUpperCase();
  const hmac = crypto.createHmac("sha256", secretKey);
  hmac.update("MATTER:" + normalized);
  return "MATTER_TOKEN_" + hmac.digest("hex").substring(0, 8);
}

// Session 1: Matter ACME-2024-089 enters the agent context
const token1 = tokenizeMatterId('ACME-2024-089', process.env.TOKENIZATION_KEY);
// -> "MATTER_TOKEN_f7a2c901"

// Session 2: Three weeks later, same matter identifier
const token2 = tokenizeMatterId('ACME-2024-089', process.env.TOKENIZATION_KEY);
// -> "MATTER_TOKEN_f7a2c901" (identical — deterministic)
console.log(token1 === token2); // true

This approach means the memory database contains tokens — not matter identifiers. In a document production request, the data produced contains no privileged client identifiers.

Audit trail for privilege log compliance

Every memory write, read, and deletion in Trace Continuity is logged to a governance_events table. For legal AI deployments, compliance officers can query:

  • What client data did the AI agent access, and when?
  • Were any PII types detected and redacted during this session?
  • When did retention policies trigger — and what was deleted?
# Query usage endpoint for audit data
curl -X GET "https://tracecontinuity.com/v1/usage" 
  -H "Authorization: Bearer mnm_your_admin_key"
# Response includes:
# {
#   "total_memories": 3201,
#   "memories_pii_redacted": 847,
#   "governance_events": 3841
# }

Legal tech compliance requirements in context

Requirement What governed memory provides
Attorney-client privilege PII/tokenization; no raw privileged data in storage
Client matter isolation Matter-scoped memory retrieval — architecturally enforced
Discovery of AI system logs Immutable governance_events audit trail
Data retention (matter closure) Retention policies tied to matter duration
Work product protection Agent reasoning and document analysis tokenized before storage
State bar ethics compliance AI memory decisions auditable

For law firms deploying AI agents under ABA Model Rules of Professional Conduct, the key requirement is the ability to demonstrate that AI-assisted work was conducted with appropriate safeguards.

Try it in the playground

The Playground lets you test matter-scoped memory isolation, PII detection on legal document text, and tokenization in real time.

Two tables, zero migrations, full LINQ — a .NET data engine that’s been running our production for 3 months

Today I’d like to slow down a bit and talk about redb.Core — the data engine at the heart of the RedBase ecosystem. The other pieces (redb.Route for pipelines, redb.Tsak for cluster runtime) lean on it, but this post is just about the database part.

I’ve been working on this project for several years. It started as an attempt to get rid of migrations and turned into what it is now — a typed object store for .NET over PostgreSQL and MSSQL.

It’s not a weekend prototype. The free packages on NuGet are at version 2.0, there are 43 packages across the ecosystem, the architecture went through three rewrites, and as of this week it’s been running 3 months on production at a 30-year national food distributor (more on that below).

This post is a technical walkthrough of redb.Core — what it is, how it differs from EF Core, what the generated SQL actually looks like, what the production workload looks like, and what’s shipping next.

What redb.Core actually is

github.com/redbase-app/redb — Apache 2.0

RedBase stores typed C# objects in two tables (_objects + _values) over PostgreSQL or Microsoft SQL Server. Not JSON blobs. Not JSONB. Real typed columns with FK constraints — NUMERIC(38,18) for money, timestamptz for dates, uuid for GUIDs. Real B-tree indexes. ACID transactions.

The schema is your C# class:

[RedbScheme("Employee")]
public class EmployeeProps
{
    public string FirstName   { get; set; } = "";
    public string LastName    { get; set; } = "";
    public int    Age         { get; set; }
    public decimal Salary     { get; set; }
    public DateTime HireDate  { get; set; }
    public string[]? Skills   { get; set; }
    public Address? HomeAddress { get; set; }
    public Dictionary<int, decimal>? BonusByYear { get; set; }
}

That attribute is the entire schema definition. Call SyncSchemeAsync<EmployeeProps>() once — done. Add a property next sprint — redeploy, call sync again. Old objects still work. No migration files. No DBA ticket. No 2am rollback story.

// Save — entire object graph, one call
await redb.SaveAsync(employee);

// Load — full graph, arrays, dicts, nested classes — all materialized
var e = await redb.LoadAsync<EmployeeProps>(id);

// Query — real LINQ, real SQL
var seniors = await redb.Query<EmployeeProps>()
    .Where(e => e.Salary > 100_000 && e.Age >= 35)
    .OrderByDescending(e => e.Salary)
    .Take(50)
    .ToListAsync();

No DbContext. No Include chains. No Add-Migration. No mapper layer.

Object graphs in one call

This is the part that surprises people coming from EF. Props can contain other Props — single references, arrays, dictionaries — and the entire graph saves and loads as one operation:

[RedbScheme("Order")]
public class OrderProps
{
    public Customer Customer { get; set; }                          // nested class
    public Address ShippingAddress { get; set; }                    // nested class
    public Product[] Products { get; set; }                         // array of classes
    public RedbObject<PaymentProps>[] Payments { get; set; }        // array of full objects with own IDs
    public Dictionary<string, RedbObject<CouponProps>> Coupons { get; set; }  // dict of objects
    public Dictionary<(int Year, string Quarter), string> Reviews { get; set; } // tuple-key dict
}

await redb.SaveAsync(order);   // entire graph persisted, FK ordering handled
var loaded = await redb.LoadAsync<OrderProps>(id);
// loaded.Props.Customer.Address — ready
// loaded.Props.Payments[0].Props — ready (full RedbObject with own Id, DateCreate, etc.)
// loaded.Props.Coupons["SUMMER20"].Props — ready

In EF Core this would be 28 tables, ~40 Include/ThenInclude calls, manual junction tables for the many-to-many, and INSERT ordering that breaks every time someone adds a non-nullable FK without a default.

In RedBase: one SaveAsync, one LoadAsync. The nested RedbObject instances are real first-class objects — they have their own IDs, their own timestamps, they can be queried independently, they participate in tree structures. They are not denormalised JSON glued to the parent.

What the LINQ actually compiles to

Where(e => e.Salary > 100_000 && e.Age >= 35) doesn’t get serialized to JSON and re-parsed (that’s the Free engine’s path — covered later). In Pro, the C# expression tree is walked node-by-node and emitted as parameterized SQL. Roughly:

WITH pvt AS (
  SELECT v._id_object,
         (array_agg(v._Numeric) FILTER (WHERE v._id_structure = $1))[1] AS "Salary",
         (array_agg(v._Long)    FILTER (WHERE v._id_structure = $2))[1] AS "Age"
    FROM _values v
   WHERE v._id_structure = ANY($3::bigint[])
     AND v._id_object IN (SELECT o._id FROM _objects o WHERE o._id_scheme = $4)
   GROUP BY v._id_object
)
SELECT o.*
  FROM _objects o
  JOIN pvt ON pvt._id_object = o._id
 WHERE pvt."Salary" > $5
   AND pvt."Age"    >= $6
 ORDER BY pvt."Salary" DESC
 LIMIT 50;

Parameterized. Plan-cached by PostgreSQL. One index scan on (_id_structure, _id_object), one aggregation pass, B-tree filter on flat columns. The number of filter fields doesn’t change the shape of the query.

The C# → SQL compiler handles arithmetic (*, +, %), Math.*, String.Contains/StartsWith/Trim/ToLower, DateTime.Year/Month/..., nullable navigation (x.Address?.City) compiled to IS NOT NULL, the ternary operator compiled to CASE WHEN, StringComparison.OrdinalIgnoreCase compiled to native ILIKE, dictionary access dict["key"] compiled to pivot columns, and a few more edge cases that EF Core itself doesn’t always handle.

You can preview the SQL of any query without executing it:

var sql = await query.ToSqlStringAsync();

Like IQueryable.ToQueryString() in EF, but works for trees, GroupBy, window functions too.

Why bulk save is so fast — two tables, two streams

Something to understand before the change-tracking section: the storage layout is two tables, so SaveAsync of a batch is two bulk operations, not N round-trips.

On PostgreSQL the provider uses Npgsql’s BeginBinaryImportAsync — native COPY protocol, binary format:

// from redb.Postgres/Data/NpgsqlBulkOperations.cs
await using var writer = await conn.BeginBinaryImportAsync(
    "COPY _objects (_id, _id_parent, _id_scheme, _name, ...) FROM STDIN (FORMAT BINARY)");
foreach (var obj in objectsList) {
    await writer.StartRowAsync();
    await writer.WriteAsync(obj.Id,        NpgsqlDbType.Bigint);
    await writer.WriteAsync(obj.IdScheme,  NpgsqlDbType.Bigint);
    // ... typed writes
}
await writer.CompleteAsync();

For a batch save: one COPY stream writes the _objects rows, another writes the _values rows. Two streams, no per-row round-trips, no string-formatted INSERTs. MSSQL uses SqlBulkCopy for the same role.

This is why 1000 routes × ~40 fields = ~40,000 value rows save in tens of milliseconds inside the 200–300 ms budget. The bottleneck is the network round-trip and the COPY write, not the ORM machinery — there isn’t any ORM machinery in the hot path.

Change tracking without DbContext (Pro)

DbContext keeps an in-memory snapshot of every entity you load — that’s how it knows what changed. It’s also why it isn’t thread-safe and why the cache dies with the request.

RedBase Pro takes a different approach. With PropsSaveStrategy.ChangeTracking (Pro only; the free tier uses DeleteInsert), SaveAsync does this on a batch:

  1. One bulk SELECT of existing values for all object IDs being saved.
  2. Build two ValueTreeNode trees — one from your in-memory objects, one from the DB state.
  3. Structural diff — subtrees with matching hashes are skipped entirely (no value comparison, no child traversal). Inserts, updates, and deletes are computed per node.
  4. Three bulk operations — BulkInsertValuesAsync, BulkUpdateValuesAsync, BulkDeleteValuesAsync — each a single round-trip. Inserts go through COPY BINARY again.

Net effect: changing one field in a deeply nested object emits one UPDATE, not a full delete-and-reinsert of the entire props graph. And the comparison happens in C# on the application side — no DbContext lifetime to worry about, safe to run from a background Channel consumer or a Parallel.ForEach.

(In the production code from the previous section, the application also does its own obj.ComputeHash() check at the route level. That part is optional business-code — you could call SaveAsync on every route and the Pro tree-diff would still skip unchanged values internally. It’s there as a coarse pre-filter so unchanged routes don’t even enter the save pipeline at all, saving the diff work too.)

Production deployment — the numbers

The biggest deployment right now:

  • 30-year national HoReCa food distributor
  • ~150,000 orders/month, ~20,000 B2B customers, 600+ cities
  • 3-node cluster, 4 cores / 8 GB RAM / 50 GB SSD per node
  • ~550 daily internal users (operators, drivers, supervisors, dispatch, back-office)
  • 10–15% CPU under full load
  • Integrations: SAP, Kafka, RabbitMQ, GPS feeds, Mercury / EGAIS / government APIs

Three months in production, no data-layer incidents. Two projects in the company use it, the second one came after the first one proved stable.

Real workload, real timings

The hottest pipeline is the SAP monitoring sync. Every 60 seconds a SQL polling consumer calls a stored procedure on SAP S/4 (usp_TsUM_MonitoringReport_xml), gets ~1000 transportation orders back as XML, syncs reference dictionaries (drivers, vehicles, list items), bulk-loads existing routes from RedBase, hash-compares each one, and saves only the changed ones.

The whole loop fits in ~200–300 ms for ~1000 routes. The actual production code:

From("direct://tsum")
    .RouteId("tsum-processing")
    .ProcessWithRedb(async (redb, exchange, ct) =>
    {
        var orders = (List<TransportationOrder>)exchange.In.Body;

        var sw = Stopwatch.StartNew();

        // 1. Sync dictionaries (Drivers, Vehicles, ListItems) — only new/changed
        var dicts = await DictionarySyncService.SyncFromOrdersAsync(redb, orders, ct);
        if (dicts.DriversNew + dicts.DriversChanged + dicts.VehiclesNew
            + dicts.VehiclesChanged + dicts.ListItemsRelinked > 0)
            await RefDataCache.RefreshAsync(redb);
        var syncMs = sw.ElapsedMilliseconds;

        // 2. Bulk load existing routes by Code — one query, ~1000 codes
        var codes = orders.Select(o => o.Code).ToList();
        var existing = await redb.Query<TransportationRoute>()
            .WhereRedb(o => codes.Contains(o.ValueString!))
            .ToListAsync();
        var existingByCode = existing.ToDictionary(o => o.ValueString!, o => o);

        // 3. Merge: update if hash changed, insert if new, skip if unchanged
        var toSave = new List<IRedbObject>();
        var updatedCount = 0;
        var skippedCount = 0;
        foreach (var order in orders)
        {
            var routeProps = MapOrderToRouteProps(order, dicts);
            if (existingByCode.TryGetValue(order.Code, out var obj))
            {
                var hashBefore = obj.ComputeHash();
                EnrichRouteFromOrder(obj.Props!, routeProps);
                if (obj.ComputeHash() != hashBefore) { toSave.Add(obj); updatedCount++; }
                else skippedCount++;
            }
            else
            {
                toSave.Add(new RedbObject<TransportationRoute>
                {
                    name = $"Route {order.Code}",
                    value_string = order.Code,
                    Props = routeProps
                });
            }
        }

        // 4. One batched save — mixed inserts + updates
        if (toSave.Count > 0)
            await redb.SaveAsync(toSave);

        Logger.LogInformation(
            "[TSUM] orders={Orders} routes(+{Created} ~{Updated} ={Skipped}) " +
            "drivers(+{DN} ~{DC}) vehicles(+{VN} ~{VC}) " +
            "sync={Sync} query={Query} save={Save} total={Total}ms",
            orders.Count, toSave.Count - updatedCount, updatedCount, skippedCount,
            dicts.DriversNew, dicts.DriversChanged,
            dicts.VehiclesNew, dicts.VehiclesChanged,
            syncMs, queryMs, saveMs, sw.ElapsedMilliseconds);
    });

TransportationRoute has ~40 fields and 12 RedbListItem references (Driver, Vehicle, CarMark, ShippingPoint, BusinessType, PlaceTo, PlaceFrom, LoadingZone, TransportStatus, Risk, DeliveryStatus, LoadStatus) plus 2 object references to AD users. Every single one is a foreign key in the database. None of them require a JOIN at query time — the materializer handles it.

Smaller queries (point-lookup of a single route, dictionary fetch, REST endpoints for the UI) run in 50–100 ms including HTTP overhead.

What’s actually in the database

After running this in production, the storage looks like this:

  • ~1000 transportation routes/day, plus delivery points, transport snapshots, garage states, slice settings, slice snapshots, drivers, vehicles, yard places, AD user refs — about a dozen [RedbScheme] classes in active use
  • ~500,000 objects in _objects (routes, points, snapshots, dictionary items)
  • ~15,000,000 rows in _values (every typed property of every object)
  • All of that lives in 2 tables — _objects and _values — plus the system tables for schemes, structures, lists, users, permissions

15 million typed value rows. Two tables. No 30-table schema. No 200 migration files. The query times above are on this dataset.

If you tried to model the same domain in EF Core flat tables, you’d end up with roughly: Routes, RoutePoints, TransportSnapshots, GarageStates, SliceSettings, SliceSnapshots, Drivers, Vehicles, CarMarks, ShippingPoints, BusinessTypes, YardPlaces, LoadingZones, TransportStatuses, DeliveryStatuses, Risks, TripRisks, AdUsers — plus junction tables and audit tables for each. 30+ tables, dozens of migrations, and every schema change is a deploy event.

In RedBase the schema change is git push. The next InitializeAsync() call adds the new structure rows. Done.

What would EF Core look like here?

I asked myself the same question before starting. Let’s count what EF would need:

  • TransportationRoute entity → 1 table
  • 12 ListItem references → 12 lookup tables + 12 nullable FKs
  • 2 AD user references → 2 more FKs
  • The 1000-route batch update → 1000 entities tracked in DbContext for change detection
  • Hash-based skip of unchanged objects → doesn’t exist in EF out of the box; you write it manually
  • The same logic across multiple processes/routes → DbContext is per-request, the change-tracker cache dies at the end of every batch

The realistic EF flow looks like this:

// Load existing — needs Include for every lookup or you get N+1
var existing = await db.Routes
    .Include(r => r.Driver)
    .Include(r => r.Vehicle)
    .Include(r => r.CarMark)
    .Include(r => r.ShippingPoint)
    .Include(r => r.BusinessType)
    .Include(r => r.PlaceTo)
    .Include(r => r.PlaceFrom)
    .Include(r => r.LoadingZone)
    .Include(r => r.TransportStatus)
    .Include(r => r.Risk)
    .Include(r => r.DeliveryStatus)
    .Include(r => r.LoadStatus)
    .Include(r => r.RiskSetBy)
    .Include(r => r.KcResponsible)
    .Where(r => codes.Contains(r.Code))
    .ToListAsync();

// Map, mutate, SaveChanges — 1000 tracked entities, full diff snapshot in RAM
// SaveChanges fires N UPDATE statements (one per row) in a transaction
await db.SaveChangesAsync();

In practice on a workload like this:

  • The Include chain on 14 lookups produces a query that PostgreSQL/MSSQL can’t always optimise cleanly (cartesian explosion risk; you split-query and pay multiple round trips).
  • SaveChanges on 1000 modified entities emits 1000 individual UPDATEs unless you reach for EFCore.BulkExtensions or similar third-party libraries. RedBase ships COPY BINARY for inserts and a batched UPDATE path for updates in the box.
  • The DbContext snapshot of 1000 tracked entities, each with 14 included lookups, is real memory pressure. AsNoTracking() is faster but you lose change detection — you have to re-implement it.
  • The 200–300 ms budget on a 4-core container is not realistic in this scenario without significant manual optimisation and probably a separate bulk-update path.

I’m not saying EF can’t do it. I’m saying the equivalent EF implementation is more code, more moving parts, and significantly slower without third-party packages. RedBase ships a single SaveAsync(toSave) and a hash-based skip primitive in the box.

The hash comparison itself (obj.ComputeHash() != hashBefore) is application-level business code — it’s optional; the Pro tree-diff inside SaveAsync would skip unchanged values anyway. But used like this it lets the application skip the ~95% of routes that didn’t change between SAP polls before they even enter the save pipeline. Combined with COPY BINARY into two tables and the Pro tree-diff for the routes that do change, the whole loop fits the 200–300 ms budget. No equivalent set of built-in primitives exists in EF Core; you either snapshot manually, re-save everything, or pull in third-party bulk packages.

What EF Core does with complex objects (vs what RedBase does)

The E000 benchmark uses EmployeeProps — a realistic model with nested classes, arrays, dictionaries, and RedbObject references:

What classic ORM needs RedBase
~28 tables 2 tables
FK ordering on every INSERT Single SaveAsync
40+ Include/ThenInclude calls LoadAsync<T>(id) — one line
Migration file for every new field Add property, call SyncSchemeAsync, done
~5,000 INSERTs for 100 employees 1 BulkInsert via COPY protocol

For 100 complex employees: EF Core ≈ 4,000–6,000 INSERTs across 28 tables in FK order. RedBase: ~3,000 typed value rows, one COPY command.

And the test results are published — 525 automated tests across all editions and both databases. All green.

The query engine: free vs Pro

This is where it gets technically interesting.

Free edition compiles your LINQ lambda to a JSON facet format, then calls a plpgsql stored procedure that parses that JSON and generates SQL dynamically. For simple 1–2 field filters, PostgreSQL optimizes correlated EXISTS subqueries well. It works.

Pro edition walks the C# expression tree directly in C# (ExpressionToSqlCompiler), emits native parameterized SQL. Same plan every call — PostgreSQL can cache it. 3–10× faster on complex multi-field filters.

But here’s what’s happening right now: I’m porting the Pro PVT CTE query engine into the free tier. The plan is full parity on PVT — same CTE shape, same expression coverage, same projection capabilities. Free and Pro will speak the same query dialect.

The remaining differences between Free and Pro will be elsewhere:

  • Query plan caching. Pro builds parameterized SQL in C# (ExpressionToSqlCompiler) — same shape every call, PostgreSQL caches the plan once and reuses it. The free tier generates SQL inside plpgsql with literal values inlined (properly escaped to prevent injection), so each call can produce a slightly different plan. Same correctness, different plan-cache behavior.
  • Materialization. Pro has a parallel materializer (multiple values streams hydrated concurrently into the object graph). Free uses a simpler sequential path.
  • Save strategy. PropsSaveStrategy.ChangeTracking (tree-diff with hash-based subtree skip) is Pro-only. Free uses DeleteInsert — still bulk via COPY, but no per-field diff.

So Pro stays faster on hot paths and large object graphs, but the query language itself — what you can write in .Where, .Select, projections, grouping, having, FTS, regex — will be the same on both sides.

Show me the SQL it generates

Here’s a real example — a projection query on EmployeeProps with 16 computed columns:
full names, salary calculations, date extractions, seniority classification via CASE, coalesce for nullable fields, explicit type cast. This is the free tier:

WITH _pvt_cte AS (
    SELECT
        v._id_object,
        (array_agg(v._Long)    FILTER (WHERE v._id_structure = 1000017))[1] AS "Age",
        (array_agg(v._Numeric) FILTER (WHERE v._id_structure = 1000020))[1] AS "Salary",
        (array_agg(v._DateTimeOffset) FILTER (WHERE v._id_structure = 1000018))[1] AS "HireDate",
        (array_agg(v._String)  FILTER (WHERE v._id_structure = 1000016))[1] AS "LastName",
        (array_agg(v._String)  FILTER (WHERE v._id_structure = 1000015))[1] AS "FirstName"
    FROM _values v
    WHERE v._id_structure = ANY(ARRAY[1000015,1000016,1000017,1000018,1000020]::bigint[])
      AND v._id_object IN (SELECT o._id FROM _objects o WHERE o._id_scheme = 1000014)
    GROUP BY v._id_object
)
SELECT
    o._id                                                          AS id,
    ("FirstName" || ' ' || "LastName")                            AS full_name,
    UPPER("FirstName")                                             AS upper_name,
    LENGTH("FirstName")                                            AS name_len,
    ("Salary" * 12)                                                AS yearly_x12,
    (("Salary" * 0.15) - ("Salary" * 0.013))                      AS bonus_after_tax,
    ("Salary" / ("Age" + 1))                                       AS avg_per_year,
    ("Age" % 10)                                                    AS age_mod10,
    ABS(("Age" - 35))                                              AS abs_diff,
    FLOOR(("Salary" / 2))                                          AS floor_half_salary,
    EXTRACT(YEAR  FROM "HireDate")                                 AS hire_year,
    EXTRACT(MONTH FROM "HireDate")                                 AS hire_month,
    EXTRACT(YEAR  FROM AGE("HireDate", '2026-05-20'))              AS tenure_years,
    CASE
        WHEN ("Age" >= 60) THEN 'senior'
        WHEN ("Age" >= 35) THEN 'mid'
        ELSE 'junior'
    END                                                            AS seniority,
    COALESCE("FirstName", '<unknown>')                             AS display_name,
    ("Age")::text                                                  AS age_as_text
FROM _pvt_cte
JOIN _objects o ON o._id = _pvt_cte._id_object
WHERE "Salary" > '0'::numeric
ORDER BY "Salary" DESC
LIMIT 3

One _values scan. One GROUP BY. Sixteen computed output columns. Standard SQL — no magic, fully readable in pgAdmin, fully EXPLAIN ANALYZE-able.

This query was generated by pvt_build_projection_sql() — a plpgsql function that is shipping in the next free release.

When this fits and when it doesn’t

Honest take.

Fits well: business apps with collections, nested structures, hierarchies, dictionaries, or schemas that change often. Apps where a business analyst can ask “add a field” and you want to ship the same day. Anything where you’d otherwise end up with 28 tables for one logical entity.

Doesn’t fit: flat reporting databases with five fixed tables. Use Dapper there — it’s honest and fast.

The argument I’d push back on is the common one — “for 80% of projects EF Core is enough”. In my experience, the moment you have collections inside an entity, lookup data, dictionaries, tree structures, or polymorphic types, the migration tax compounds fast. RedBase removes that whole class of work. For the remaining 20% of cases — pure flat schemas — it’s overkill, agreed.

And two more axes that usually get left out of that comparison:

  • Extensibility. Adding a field is a property in C# and one InitializeAsync() call — no migration script, no review, no deploy window. The same change in an EF stack is at minimum: model edit + Add-Migration + reviewed SQL + coordinated deploy. Multiply by every iteration in a year.
  • Cost of development. On the TsUM project the data-layer work — schemas, storage, queries, sync pipelines, change-tracking, bulk save — came in at roughly 128 person-hours. The estimate for the same scope on a classic EF + handwritten bulk-update + ASP.NET Identity + 30-table migration stack was on the order of 3000 person-hours. That’s not a typo and it’s not marketing — it’s the gap between “add a property, redeploy” and “add a column, write migration, update DTOs, update mapper, update repository, write tests for all of it”. The 80%/20% framing only holds if you count lines of code; once you count engineer-hours over a project lifetime it inverts.

What else is in the box

195+ working examples in the repo. The shortlist of things people usually don’t expect to be built-in:

// Tree queries — recursive CTE underneath, no CTE to write yourself
var products = await redb.TreeQuery<Product>(londonHQ.Id, maxDepth: 10)
    .Where(p => p.InStock)
    .ToListAsync();

// Soft delete with atomic mark + background purge + progress in DB
await redb.SoftDeleteAsync(ids, user);

Also shipping in the core (this is the short list — the real list is much longer):

  • GroupBy + window functions in one query
  • Built-in users / roles / permissions (no ASP.NET Identity dependency, no separate auth schema)
  • Export / import via .redb files (PostgreSQL ↔ MSSQL portability)
  • Multi-database in one process with full domain isolation — separate connections, separate scheme caches, separate object caches per domain; one app can talk to several independent RedBase databases at the same time without their metadata leaking into each other
  • Scheme cache and object cache (both per-domain), invalidated on SyncSchemeAsync / SaveAsync, used by the materializer to skip repeated metadata lookups
  • Atomic soft-delete with background purge and progress visible in the DB
  • Tree queries with recursive CTE
  • Polymorphic trees — different C# types at each level of the same tree

There’s a lot more that doesn’t fit in one section. The architecture page has the full map.

What’s coming next

The FreePvtQuery release is the nearest milestone. This is a complete rewrite of the free tier’s query engine based on the PVT CTE architecture — bringing it to full parity with Pro on the query side:

  • Single-pass _values scan (vs correlated EXISTS per field)
  • Full expression system: arithmetic, string, date, math, regex, FTS, CASE, coalesce, cast
  • Projections with arbitrary computed columns (like the SQL above)
  • HAVING, DISTINCT ON, GROUP BY extensions

After that: MSSQL support for the new engine, C# LINQ-to-JSON bridge so you keep writing .Where(e => e.Salary > 100_000) without touching JSON. Pro will keep its edge on plan caching (parameterized SQL), parallel materialization, and tree-diff change tracking.

This is maybe 10% of what’s in there

This post covered the basics: storage model, query engine, free PVT demo, production deployment. Topics I didn’t touch:

  • Polymorphic tree hierarchies (different C# types at every level, same tree API)
  • Object graphs with RedbObject<T> references inside Props
  • Data migrations (Pro): fluent API, computed columns, in-place type changes
  • Export/import: .redb files (JSONL/ZIP), PostgreSQL ↔ MSSQL portability
  • Multi-database in one process (domain-isolated caches)
  • redb.CLI — schema management from command line
  • redb.PropsEditor — runtime props editing UI
  • Integration with redb.Route (22-transport pipeline engine) — separate article

If there’s a direction you want covered next — migrations, polymorphic trees, benchmarks, internals of the diff-tree change tracking — drop it in the comments and that’s what the next post will be.

Links

There’s a lot of material. If you want to go deep without reading three articles, paste these into your AI of choice and ask it to analyse the architecture:

Questions in the comments — I’ll answer them directly.

Join the GitHub Finish-Up-A-Thon Challenge: $3,000 Prize Pool!

We’re thrilled to announce our newest challenge with GitHub!

Running through June 7, the GitHub Finish-Up-A-Thon Challenge invites you to finally give that abandoned side project, challenge submission, or hackathon creation the love it deserves. GitHub is looking for ways to empower builders to finish what they started, and this challenge is the first step.

There’s a $3,000 prize pool on the line for those of you who are up to the challenge. Ten winners will be selected and will also receive a DEV++ membership along with an exclusive winner badge.

Read on to learn more.

Our Prompt

Your mandate is to revive and finish a project you started but never completed.

You built something cool under pressure, likely with too little sleep and not enough time. Now it’s your chance to pick it back up, add some polish, and share it with the world.

The most important aspect? Show us your before and after, and how GitHub Copilot helped you get there.

Judging Criteria

All qualified entries will be judged based on the following criteria:

  • Use of underlying technology
  • Usability and User Experience
  • Originality and Creativity
  • Completion Arc: Is there a clear before and after journey?

Prizes

Ten Winners will each receive:

  • $300 USD
  • DEV++ Membership
  • Exclusive DEV Badge

All Participants with a valid submission will receive a completion badge on their DEV profile.

How To Participate

In order to participate, share your revived project and publish a post using the submission template below.

GitHub Finish-Up-A-Thon Challenge Submission Template

Please review our judging criteria, rules, guidelines, and FAQ page before submitting so you understand our participation guidelines and official contest rules such as eligibility requirements.

Important Dates

  • May 21: GitHub Finish-Up-A-Thon Challenge begins!
  • June 7: Submissions due at 11:59 PM PST
  • June 18: Winners Announced

We can’t wait to see what you finally ship! Questions about the challenge? Ask them below.

Good luck and happy building!