Meet Your New AI-Powered Senior DBA: Bridging the Gap Between Metadata and Architectural Excellence

Database schemas often become “black boxes” over time. They accumulate technical debt, hidden bottlenecks, and security vulnerabilities that even seasoned developers might miss during a hectic sprint.

I decided to change that.

I’m excited to introduce AI Database Analyzer & Architect, an enterprise-grade tool built with .NET MAUI and Clean Architecture that acts as an autonomous Senior Database Consultant in your pocket.

Why This Isn’t Just “Another AI Wrapper”

Unlike simple chat interfaces, this tool is designed with Architectural Intelligence. It doesn’t just read your schema; it audits it across 40+ specialized pillars including:

Performance Tuning: Detecting non-SARGable predicates, missing indexes, and trigger-induced deadlocks.

Security & Privacy: Using NER logic to find unmasked PII/PHI data and identifying SQL injection risks in stored procedures.

Scalability: Identifying horizontal scalability bottlenecks and ORM anti-patterns (like the dreaded N+1).

Data Modeling: Detecting normalization violations (3NF) and circular dependencies.

The Tech Stack
Frontend: .NET MAUI (Multi-platform) with a strict Senior-level MVVM pattern.

Hybrid AI: Toggle between Google Gemini for high-scale cloud intelligence and Ollama for local, privacy-first analysis.

Visualization: Interactive, zoomable ER diagrams powered by Mermaid.js.

Reporting: Executive-level PDF exports using QuestPDF.

Deep-Dive Audits

The analyzer goes deep. It checks for Identity Column Capacity (warning you before your INT runs out), Collation Mismatches that kill index performance, and even AI Model Collapse (detecting if your data is being polluted by synthetic LLM noise).

Open Source & Future

This project is built to bridge the gap between raw database metadata and actionable expert insights. Whether you are using SQL Server, PostgreSQL, or MySQL, this tool helps you ensure your data layer is production-ready.

Check out the repo here: https://github.com/furkiak/AIDatabaseAnalyzer

I’d love to hear your thoughts! What’s the biggest “hidden” database issue you’ve ever encountered in production? Let’s discuss in the comments!

I Built a Genetic Algorithm That Discovers Trading Strategies – Here’s What 89 Generations Found

I wanted a system that could discover trading strategies without me hand-tuning every parameter. So I built one.

finclaw is an open-source quantitative finance engine in Python. One of its core features is an evolution engine that uses genetic algorithm principles to mutate, evaluate, and improve trading strategies automatically. After running it for 89 generations on NVDA data, here’s what I learned.

The Problem With Manual Strategy Tuning

Every quant hits the same wall: you write a strategy, backtest it, tweak a parameter, backtest again. Repeat 200 times. You end up overfitting to historical data without realizing it.

I wanted something that could explore the strategy space systematically — try combinations I wouldn’t think of, and discard what doesn’t work through a principled selection process rather than gut feeling.

How The Evolution Engine Works

The core loop is deceptively simple:

  1. Seed — Start with a YAML strategy definition (entry rules, exit rules, filters)
  2. Evaluate — Backtest the strategy and compute a fitness score (return, Sharpe, drawdown, win rate)
  3. Analyze — Look at where the strategy fails (losing trade clusters, poor exits, bad market timing)
  4. Propose — Generate targeted mutations: tighten stop losses, adjust RSI thresholds, add volume filters
  5. Mutate — Apply the best proposal to create a child strategy
  6. Select — Keep a Pareto frontier of the top N strategies
  7. Repeat — Until convergence or max generations

Each strategy is a YAML file that the DSL engine compiles into executable trading rules:

name: momentum_rsi_evolved
entry:
  - rsi_14 < 35
  - ma_cross(5, 20) == "golden"
  - volume > volume_ma_20 * 1.3
exit:
  - rsi_14 > 72
  - trailing_stop: 8%
filters:
  - trend_adx_strength > 20

The mutator modifies these rules — widening RSI bands, swapping moving average periods, adding or removing filters — while the evaluator runs a full backtest on each variant.

Running It

# Install finclaw
pip install finclaw

# Evolve a strategy for 50 generations
finclaw evolve my_strategy.yaml --symbol NVDA --generations 50 --frontier-size 5

Or via the Python API:

from src.evolution.engine import EvolutionEngine, EvolutionConfig
from src.strategy.expression import OHLCVData

config = EvolutionConfig(
    max_generations=50,
    frontier_size=5,
    no_improvement_limit=10,
)
engine = EvolutionEngine(config=config)
result = engine.run(seed_strategy, my_data)

print(f"Best score: {result['best_score'].composite():.4f}")
print(f"Generations: {result['generations_run']}")

What 89 Generations Found

I seeded the engine with a basic golden-cross momentum strategy on NVDA (2022-2025 daily data) and let it run.

Generation 1 (seed): Sharpe 0.42, max drawdown -28%, win rate 38%

The seed was mediocre. Lots of whipsaw trades during the 2022 drawdown.

Generation 23: Sharpe 0.91, max drawdown -19%, win rate 44%

The engine discovered that adding a volume confirmation filter (volume > 1.5x 20-day average) eliminated most false breakouts. It also tightened the trailing stop from 12% to 8%.

Generation 56: Sharpe 1.24, max drawdown -15%, win rate 48%

A mutation added an ADX trend strength filter (ADX > 25), preventing entries during choppy sideways markets. This was the single biggest improvement — cutting drawdown by 4 percentage points.

Generation 89 (final): Sharpe 1.31, max drawdown -14%, win rate 51%

The final strategy bore little resemblance to the seed. It had evolved RSI thresholds from 30/70 to 33/68, added two filters the original didn’t have, and switched from a fixed stop loss to a trailing stop with an ATR multiplier.

The Anti-Overfitting Problem

Genetic algorithms are overfitting machines if you’re not careful. Here’s what I built to fight it:

Walk-forward validation. The evaluator doesn’t just backtest on the full dataset. It uses walk-forward splits — train on 2 years, test on the next 6 months, slide forward. The fitness score is the out-of-sample performance.

Monte Carlo stress testing. Each candidate strategy also gets run through 100 Monte Carlo shuffles to check if the equity curve is robust or just lucky.

Pareto frontier. Instead of optimizing a single metric, the frontier tracks multiple objectives (return, risk, consistency). A strategy that sacrifices some return for much lower drawdown stays in the population.

# Run with walk-forward validation (built-in)
finclaw evolve strategy.yaml --symbol AAPL --generations 30 --start 2020-01-01

Architecture

The engine is built from four pluggable components:

  • Evaluator — Runs backtests and computes FitnessScore (composite of return, Sharpe, drawdown, win rate)
  • Proposer — Analyzes failures and generates mutation candidates
  • Mutator — Applies YAML-level mutations to strategy definitions
  • Frontier — Manages the Pareto-optimal strategy set

Each component is replaceable. You can plug in your own evaluator that uses a different backtesting engine, or write a custom proposer that targets specific strategy weaknesses.

What I Learned

Volume filters matter more than entry signals. Across dozens of evolved strategies, the single most impactful mutation was always some form of volume confirmation. The market is noisy; volume tells you when the signal is real.

Stop losses evolve toward ATR-based trailing stops. Fixed percentage stops consistently get replaced by volatility-adjusted ones. Makes sense — a 5% stop is too tight for a volatile stock and too loose for a calm one.

Fewer rules beat more rules. The engine repeatedly pruned overly complex strategies. The best performers had 3-5 entry conditions, not 10. Occam’s razor, enforced by selection pressure.

Try It

finclaw is open source. The evolution engine runs on any OHLCV data — US stocks, A-shares, crypto.

pip install finclaw
finclaw evolve your_strategy.yaml --symbol AAPL --generations 20

The full codebase, including 240+ technical factors, walk-forward backtesting, and paper trading:

finclaw on GitHub | Python · 240+ factors · Evolution engine · Paper trading

If you’re interested in AI agent security (the other thing I work on), check out ClawGuard — an open-source AI agent security engine with 285+ threat patterns.

The evolution engine code lives in src/evolution/. PRs welcome.

Non-Standard ERC-20 Behavior: The Phantom Deposit Bug Class That Auditors Keep Missing

When we say “ERC-20 token,” we assume a contract that behaves according to the EIP-20 specification. But assume is the operative word. In February 2026, a non-standard transferFrom implementation in the DGLD gold-backed token let attackers mint 100 million unbacked tokens on Base — against a legitimate circulating supply of just 70.8 tokens on that chain.

The vulnerability wasn’t in the bridge. It wasn’t in the L2. It was in a transferFrom function that returned true without actually moving tokens. And it had passed two independent audits.

This article isn’t about DGLD specifically — it’s about the ERC-20 compliance gap that exists across thousands of deployed tokens, and the concrete audit techniques that catch it.

The ERC-20 Trust Contract

Every protocol that integrates external tokens makes implicit assumptions:

transferFrom(from, to, amount) returns true
  → "amount" tokens moved from "from" to "to"
  → balanceOf(from) decreased by amount
  → balanceOf(to) increased by amount

The EIP-20 spec says transferFrom SHOULD throw on failure. It says nothing about what happens when a contract returns true but doesn’t enforce the transfer. This ambiguity has created at least three distinct exploit patterns since 2022.

Pattern 1: Silent Success (DGLD, Feb 2026)

The DGLD Ethereum contract inherited legacy code where transferFrom could execute successfully — returning true to the caller — without enforcing that the token balance actually changed. The Ethereum↔Base bridge (built on OP Stack) interpreted that true as confirmation that tokens were locked on L1, and minted the corresponding representation on L2.

The attack flow:

Attacker → DGLD.transferFrom(attacker, bridge, 100M)
         → Returns true (no revert)
         → But attacker's balance: unchanged
         → Bridge sees: "100M DGLD deposited"
         → Base mints: 100M unbacked DGLD
         → Attacker dumps on Aerodrome DEX → drains USDC pools

The bridge did nothing wrong by its own logic. It called a standard ERC-20 function, got a success response, and acted accordingly. The fault was in the semantic gap between what transferFrom promised and what it delivered.

Pattern 2: Fee-on-Transfer Deflation

Tokens like USDT and STA apply transfer fees, meaning balanceOf(to) increases by less than amount. Protocols that don’t check post-transfer balances end up with phantom accounting:

// VULNERABLE: Assumes full amount arrived
function deposit(address token, uint256 amount) external {
    IERC20(token).transferFrom(msg.sender, address(this), amount);
    balances[msg.sender] += amount; // May be more than actually received
}

Pattern 3: Return Value Amnesia

Some tokens (notably older USDT on Ethereum) don’t return a boolean at all. Calling them with IERC20(token).transferFrom(...) reverts at the ABI decoding level if the contract expects a bool return. This is why OpenZeppelin’s SafeERC20 exists — but many protocols still don’t use it.

The Audit Gap: Why This Keeps Slipping Through

The DGLD vulnerability passed two independent audits and an external review in Q4 2025. How?

1. Inherited Code Blind Spots

The vulnerable transferFrom logic came from a Consensys implementation deployed in February 2022. Four years of production use created a false sense of security. Auditors focused on newer code (the Base deployment logic) and treated the ERC-20 base as “battle-tested.”

Lesson: Legacy code isn’t safe code. Every audit should include the token’s core transfer functions, regardless of how long they’ve been deployed.

2. Specification vs. Implementation Drift

The EIP-20 spec is deliberately minimal. It says:

  • transferFrom SHOULD throw if the sender doesn’t have enough tokens
  • It MUST fire a Transfer event

It does NOT say:

  • transferFrom MUST revert on insufficient balance (SHOULD ≠ MUST)
  • The return value MUST accurately reflect whether tokens moved
  • The function MUST be atomic (balance changes happen or they don’t)

Any contract that exploits these gaps is “ERC-20 compliant” by the letter of the spec while being functionally broken.

3. Integration Context Matters

A transferFrom that returns true without moving tokens is harmless in isolation. It only becomes exploitable when another contract trusts that return value to gate a consequential action (minting, unlocking, accounting). Auditors reviewing the token contract alone might flag it as “non-standard but not exploitable.” Auditors reviewing the bridge alone see a standard ERC-20 integration. Neither catches the composition.

The 7-Point ERC-20 Compliance Audit Checklist

After analyzing every ERC-20 integration exploit from 2022-2026, here’s the minimum verification set:

1. Balance Delta Verification

Never trust the return value. Check actual balance changes:

function _safeTransferIn(
    address token,
    address from,
    uint256 amount
) internal returns (uint256 received) {
    uint256 balBefore = IERC20(token).balanceOf(address(this));
    IERC20(token).safeTransferFrom(from, address(this), amount);
    received = IERC20(token).balanceOf(address(this)) - balBefore;
    require(received > 0, "Zero transfer");
}

This single pattern prevents phantom deposits, fee-on-transfer accounting errors, and rebasing token surprises.

2. Return Value Handling

Use SafeERC20 for all external token calls. It handles:

  • Tokens that return bool (standard)
  • Tokens that return nothing (USDT-style)
  • Tokens that revert on failure
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

using SafeERC20 for IERC20;

// Handles all three return patterns
token.safeTransferFrom(from, to, amount);

3. Allowance Behavior Testing

Some tokens (USDT again) require allowance to be reset to 0 before setting a new value. Test for:

function testAllowanceReset() public {
    token.approve(spender, 100);
    // Some tokens revert here if allowance != 0
    token.approve(spender, 200);
}

Use safeIncreaseAllowance / forceApprove to handle this.

4. Reentrancy via Transfer Hooks

ERC-777 tokens and some ERC-20 tokens with hooks (like stETH’s rebase) can trigger reentrancy through tokensReceived callbacks:

// VULNERABLE to ERC-777 reentrancy
function deposit(address token, uint256 amount) external {
    IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
    // Attacker's tokensReceived() hook fires HERE
    // State not yet updated
    balances[msg.sender] += amount;
}

// SAFE: CEI pattern
function deposit(address token, uint256 amount) external nonReentrant {
    balances[msg.sender] += amount; // State update first
    IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
    // Hook fires after state is consistent
}

5. Decimals Assumption Testing

Not all tokens use 18 decimals. USDC uses 6. Some use 2 or 8. Test arithmetic with varying decimal counts:

function testFuzz_decimalVariance(uint8 decimals) public {
    vm.assume(decimals <= 24);
    MockToken token = new MockToken(decimals);
    uint256 amount = 1000 * 10**decimals;

    // Verify your math doesn't overflow/underflow
    protocol.deposit(address(token), amount);
    assertEq(protocol.getBalance(address(token)), amount);
}

6. Max Supply / totalSupply Manipulation

Rebasing tokens (stETH, aTokens) change balances without transfers. Elastic supply tokens can inflate totalSupply. Test that your protocol doesn’t depend on:

  • totalSupply being constant
  • balanceOf not changing between transactions
  • Transfer amounts equaling balance deltas

7. Blocklist / Pausability Awareness

USDC, USDT, and many tokens can freeze individual addresses or pause all transfers. Your protocol should handle:

  • transferFrom reverting unexpectedly
  • Funds becoming temporarily locked
  • Withdrawal functions that don’t brick when one token is paused

Automated Detection: Foundry Integration Tests

Here’s a reusable Foundry test suite for ERC-20 compliance verification:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

abstract contract ERC20ComplianceTest is Test {
    IERC20 public token;
    address public alice = makeAddr("alice");
    address public bob = makeAddr("bob");

    // Override in concrete test to set the token
    function _setupToken() internal virtual;

    function setUp() public {
        _setupToken();
    }

    function test_transferFromActuallyMovesTokens() public {
        uint256 amount = 1000;
        deal(address(token), alice, amount);

        vm.prank(alice);
        token.approve(bob, amount);

        uint256 aliceBefore = token.balanceOf(alice);
        uint256 bobBefore = token.balanceOf(bob);

        vm.prank(bob);
        bool success = token.transferFrom(alice, bob, amount);

        if (success) {
            assertEq(
                token.balanceOf(alice), 
                aliceBefore - amount,
                "transferFrom returned true but sender balance unchanged"
            );
            assertGe(
                token.balanceOf(bob),
                bobBefore,
                "transferFrom returned true but receiver lost tokens"
            );
        }
    }

    function test_transferFromExceedingBalanceReverts() public {
        deal(address(token), alice, 100);

        vm.prank(alice);
        token.approve(bob, type(uint256).max);

        uint256 aliceBal = token.balanceOf(alice);

        vm.prank(bob);
        try token.transferFrom(alice, bob, aliceBal + 1) returns (bool success) {
            assertFalse(
                success,
                "CRITICAL: transferFrom succeeded with insufficient balance"
            );
            assertEq(token.balanceOf(alice), aliceBal, "Balance changed on failed transfer");
        } catch {
            // Reverting is the expected safe behavior
        }
    }

    function test_feeOnTransferDetection() public {
        uint256 amount = 10000;
        deal(address(token), alice, amount);

        vm.prank(alice);
        token.approve(bob, amount);

        uint256 bobBefore = token.balanceOf(bob);

        vm.prank(bob);
        token.transferFrom(alice, bob, amount);

        uint256 received = token.balanceOf(bob) - bobBefore;

        if (received < amount) {
            emit log_named_uint("FEE-ON-TRANSFER DETECTED. Fee", amount - received);
        }
    }
}

Running Against Live Tokens

# Fork mainnet and test against real USDT
forge test --match-contract USDTComplianceTest 
  --fork-url $ETH_RPC_URL 
  --fork-block-number 19500000 -vvv

Bridge-Specific Defenses

The DGLD exploit specifically targeted the L1→L2 bridge trust boundary. For bridge developers:

Defense 1: Balance Delta at the Bridge Level

function depositERC20(
    address token,
    uint256 amount,
    uint32 destChainId
) external {
    uint256 balBefore = IERC20(token).balanceOf(address(this));
    IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
    uint256 actualDeposited = IERC20(token).balanceOf(address(this)) - balBefore;

    // Mint on L2 based on ACTUAL deposit, not claimed amount
    _sendCrossChainMessage(destChainId, msg.sender, token, actualDeposited);
}

Defense 2: Token Allowlisting with Compliance Verification

struct TokenConfig {
    bool allowed;
    bool feeOnTransfer;
    bool rebasable;
    uint256 maxSingleDeposit;
    uint256 dailyDepositCap;
}

mapping(address => TokenConfig) public tokenConfigs;

modifier onlyCompliantToken(address token) {
    require(tokenConfigs[token].allowed, "Token not allowlisted");
    _;
}

Defense 3: Cross-Chain Balance Reconciliation

Run continuous reconciliation between L1 locked balances and L2 minted supply:

# Simplified reconciliation check
def reconcile(token_l1, token_l2, bridge_l1):
    locked_l1 = token_l1.balanceOf(bridge_l1)
    minted_l2 = token_l2.totalSupply()

    delta = abs(locked_l1 - minted_l2)
    threshold = locked_l1 * 0.001  # 0.1% tolerance for timing

    if delta > threshold:
        alert(f"RECONCILIATION FAILURE: L1={locked_l1}, L2={minted_l2}")
        pause_bridge()

DGLD’s monitoring detected the anomaly within ~2.5 hours. With real-time reconciliation, this window could have been minutes.

Key Takeaways

  1. Never trust ERC-20 return values alone. Always verify with balance deltas. The transferFrom → true → tokens moved assumption has failed repeatedly.

  2. Audit inherited code with the same rigor as new code. DGLD’s vulnerability was 4 years old, passed 2 audits, and lived in “battle-tested” Consensys code. Legacy ≠ safe.

  3. Composition creates vulnerabilities that isolation doesn’t reveal. A non-standard transferFrom is harmless alone but catastrophic when a bridge trusts its return value. Audit at the integration boundary, not just the component level.

  4. Every token integration is a trust boundary. When your contract calls transferFrom on an external token, you’re trusting that token’s implementation. Verify, don’t trust.

  5. Balance delta checks are the single highest-ROI defensive pattern in DeFi. They defend against phantom deposits, fee-on-transfer errors, rebasing surprises, and non-standard implementations — all in one pattern.

This research is part of the DeFi Security Research series by DreamWork Security. Follow for weekly deep dives into smart contract vulnerabilities, audit techniques, and defensive patterns.

Self-Hosted File Servers: From FTP to WebDAV to Cloud-Native

Every developer hits the same question eventually: where do I put the files?

Maybe it’s backup artifacts from a CI pipeline. Maybe it’s a shared folder for a small team. Maybe you’re tired of paying per-seat for Dropbox and want something you actually control.

The self-hosted file server space has matured a lot, but the sheer number of options can be overwhelming. This article breaks down the main approaches, when each one makes sense, and for the WebDAV path, walks you through two open-source projects I maintain that cover local and cloud storage scenarios.

The Landscape: How Do You Want to Serve Files?

Before picking a tool, it helps to understand the categories. Self-hosted file sharing broadly falls into four buckets:

1. Traditional File Protocols (FTP/SFTP/SMB)

The old guard. FTP and SFTP are battle-tested and universally supported. SMB (Samba) is the default for Windows network shares. They work, but they come with baggage: FTP sends credentials in plaintext (unless you configure FTPS), SMB is chatty on the network and painful to expose over the internet, and neither protocol plays well with modern auth flows like OAuth or LDAP without extra tooling.

Best for: LAN-only file sharing, legacy system integration, printer/scanner destinations.

2. Full-Featured Cloud Platforms (Nextcloud, Seafile, FileRun)

These are the “replace Google Drive” solutions. Nextcloud in particular has become the go-to self-hosted platform, offering file sync, calendars, contacts, office document editing, and hundreds of plugins. Seafile focuses more narrowly on file sync with excellent delta-sync performance. FileRun takes a lighter approach, letting you manage files directly on the filesystem with a polished web UI.

The trade-off is complexity. Nextcloud needs a database, a PHP runtime, and regular maintenance. Seafile’s block-based storage means your files aren’t directly accessible on the filesystem. These platforms are excellent when you need the full suite, but they’re overkill if you just want a folder accessible over the network.

Best for: Teams that need a full collaboration platform, users migrating from Google Workspace or Dropbox.

3. Sync-First Tools (Syncthing, Resilio)

These aren’t servers in the traditional sense. They’re peer-to-peer sync engines. Syncthing is fully open-source and does one thing well: keep folders in sync across devices without a central server. There’s no web UI for browsing files, no user management, no access control. It’s a pipe, not a platform.

Best for: Keeping personal devices in sync, distributed backups, scenarios where no central server is desired.

4. WebDAV

And then there’s WebDAV, the protocol that sits in the sweet spot between “too simple” and “too complex.” WebDAV extends HTTP with file operations: upload, download, move, copy, lock, list directories. Every major OS supports it natively (mount it as a network drive on Windows, macOS, or Linux without installing anything). Backup tools like Rclone, Restic, and Duplicati speak WebDAV out of the box. Office applications can open and save documents directly over WebDAV URLs.

Because it’s just HTTP under the hood, you get all the benefits of the HTTP ecosystem: reverse proxies, TLS termination, OAuth/OIDC integration, load balancing, caching headers. All of these work without any protocol translation.

Best for: Lightweight file sharing with real access control, backup destinations, CI/CD artifact storage, mobile file access, mounting remote storage as a local drive.

Why I Built Two WebDAV Servers

I’ve been running WebDAV servers in various setups for a while. My original project, webdav-server, started as a simple Docker container wrapping Apache httpd with WebDAV modules. Over time it grew to support per-folder permissions, multiple auth methods, and a bunch of operational features.

But there was a gap: what if your files live in S3 or Google Cloud Storage? Apache’s WebDAV module only works with local filesystems. So I built a second project, cloud-webdav-server, written in Go, that presents cloud object storage as a WebDAV drive.

Here’s how the two compare:

webdav-server cloud-webdav-server
Runtime Apache httpd (Docker) Go binary (distroless Docker, ~10 MB)
Storage Local filesystem Local, Amazon S3/MinIO, Google Cloud Storage
Auth Basic, LDAP, OAuth/OIDC, LDAP+Basic fallback Basic, LDAP, OpenID Connect (Bearer)
Permissions Per-folder, per-user, exclusions Per-folder, per-user, exclusions
Kubernetes OCI Helm chart OCI Helm chart
Best for On-premise file sharing, NAS replacement Cloud-native setups, S3/GCS frontends

Both share the same FOLDER_PERMISSIONS syntax, so the mental model transfers between them.

Getting Started with webdav-server

👉 github.com/vaggeliskls/webdav-server
📖 Full documentation

The quickest way to get a WebDAV server running:

1. Create a .env file:

BASIC_AUTH_ENABLED=true
BASIC_USERS="alice:alice123 bob:bob123"
FOLDER_PERMISSIONS="/shared:*:ro,/alice:alice:rw,/bob:bob:rw"

2. Create a docker-compose.yml:

services:
  webdav:
    image: ghcr.io/vaggeliskls/webdav-server:latest
    ports:
      - 80:8080
    volumes:
      - ./webdav-data:/var/lib/dav/data
    env_file:
      - .env

3. Run it:

docker compose up -d

That’s it. Alice and Bob can both read /shared, but each can only write to their own folder. Folders are auto-created at startup.

Per-Folder Access Control

This is the feature that sets it apart from most WebDAV containers you’ll find on Docker Hub. The FOLDER_PERMISSIONS variable uses a simple format:

/path:users:mode

Where users can be public (no auth), * (any authenticated user), specific usernames, or exclusions with !:

# Everyone can read /public, no login needed
# All authenticated users can read /shared except charlie
# Only alice and bob can write to /projects
FOLDER_PERMISSIONS="/public:public:ro,/shared:* !charlie:ro,/projects:alice bob:rw"

Authentication Options

You can enable one or more auth methods:

  • Basic Auth: simplest setup, users defined in the .env file with bcrypt hashing
  • LDAP: connect to Active Directory or any LDAP server for centralized user management
  • OAuth/OIDC: integrate with Keycloak, Okta, Azure AD, or any OpenID Connect provider
  • LDAP + Basic fallback: try LDAP first, fall back to local users if LDAP auth fails

What’s New Since v1

Since I first wrote about this project, several features have been added:

  • Per-folder permissions with user inclusion/exclusion
  • Public folders that require no authentication
  • Read-only vs read-write mode per folder (configurable HTTP methods)
  • CORS support for web clients
  • Health check endpoint (/_health) for load balancer probes
  • LDAP + Basic fallback authentication
  • Security test suite for validating access control
  • Full documentation site at vaggeliskls.github.io/webdav-server

Getting Started with cloud-webdav-server

👉 github.com/vaggeliskls/cloud-webdav-server

If your files live in S3 or GCS, this project lets you access them over WebDAV without syncing anything locally.

Local storage (same idea, lighter runtime):

STORAGE_TYPE=local
LOCAL_DATA_PATH=/data
BASIC_AUTH_ENABLED=true
BASIC_USERS="alice:alice123"
FOLDER_PERMISSIONS="/files:*:rw"

Amazon S3 / MinIO:

STORAGE_TYPE=s3
S3_BUCKET=my-bucket
S3_REGION=us-east-1
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
FOLDER_PERMISSIONS="/files:*:rw"

Google Cloud Storage:

STORAGE_TYPE=gcs
GCS_BUCKET=my-bucket
GOOGLE_APPLICATION_CREDENTIALS=/run/secrets/sa.json
FOLDER_PERMISSIONS="/files:*:rw"

Then just docker compose up and mount http://localhost:8080/files/ as a network drive. Your S3 bucket is now a WebDAV folder.

The Go binary compiles into a ~10 MB distroless container that runs as a non-root user, lean enough for Kubernetes sidecars or edge deployments.

Kubernetes Deployment with OCI Helm Charts

Both projects ship with Kubernetes Helm charts distributed as OCI artifacts. This means you can deploy either WebDAV server to your cluster using standard Helm commands without adding a custom chart repository:

# Deploy webdav-server
helm install webdav oci://ghcr.io/vaggeliskls/charts/webdav-server

# Deploy cloud-webdav-server
helm install cloud-webdav oci://ghcr.io/vaggeliskls/charts/cloud-webdav-server

OCI-based Helm charts are pulled directly from the container registry, so you get the same versioning and distribution model as your container images. Override values with -f values.yaml or --set flags as usual.

Practical Use Cases

Here are some real scenarios where a WebDAV server fits perfectly:

Backup destination. Tools like Rclone, Restic, and Duplicati support WebDAV natively. Point them at your server and you have a self-hosted backup target with access control.

CI/CD artifact storage. Upload build artifacts with curl -T after a successful build, download them in downstream jobs. Give the CI bot write access and everyone else read-only:

FOLDER_PERMISSIONS="/artifacts:ci-bot:rw,/artifacts:*:ro"

Mobile file access. Apps like Solid Explorer (Android) and Documents by Readdle (iOS) can connect to WebDAV servers directly. No app-specific sync client needed.

Network drive replacement. Mount it on Windows (net use Z: http://server/files/), macOS (Finder > Connect to Server), or Linux (mount -t davfs). Native OS support, no client installation.

Document collaboration. LibreOffice and Microsoft Office can open and save files directly over WebDAV URLs. No download-edit-upload cycle.

Static file distribution. Serve public read-only assets (releases, datasets, documentation) without any authentication overhead. Combine with a reverse proxy for caching and HTTPS.

Choosing the Right Approach

Scenario Recommended approach
Full Google Drive replacement with office editing, calendars, etc. Nextcloud or Seafile
Syncing folders between personal devices Syncthing
Lightweight file sharing with access control webdav-server
Serving files from S3/GCS over WebDAV cloud-webdav-server
LAN-only sharing with Windows machines SMB/Samba
Legacy system integration FTP/SFTP
CI/CD artifacts, backup targets, mobile access webdav-server or cloud-webdav-server

There’s no single right answer. It depends on what you’re storing, who needs access, and how much infrastructure you want to manage. But if you need something between “raw FTP” and “full Nextcloud deployment,” WebDAV hits a sweet spot that’s worth considering.

Links

If you find these useful, a ⭐ on GitHub is always appreciated. Issues and PRs are welcome.

Ollama Has a Free API — Run LLMs Locally with One Command

TL;DR

Ollama lets you run large language models locally on your machine. One command to download and run Llama 3, Mistral, Gemma, Phi, and 100+ models — with an OpenAI-compatible API.

What Is Ollama?

Ollama makes local AI simple:

  • One command — ollama run llama3 and you’re chatting
  • 100+ models — Llama 3, Mistral, Gemma, Phi, CodeLlama, etc.
  • OpenAI-compatible API — drop-in replacement at localhost:11434
  • GPU acceleration — NVIDIA, AMD, Apple Silicon
  • Model customization — Modelfiles for custom system prompts
  • Free — MIT license, runs on your hardware

Quick Start

# Install
curl -fsSL https://ollama.com/install.sh | sh
# Or: brew install ollama

# Run a model (auto-downloads)
ollama run llama3.1

# Run smaller models for faster responses
ollama run phi3        # 3.8B — fast, good for coding
ollama run mistral     # 7B — great general purpose
ollama run gemma2      # 9B — Google's model
ollama run codellama   # For code generation

REST API

# Chat completion
curl http://localhost:11434/api/chat -d '{
  "model": "llama3.1",
  "messages": [{"role": "user", "content": "Why is the sky blue?"}],
  "stream": false
}'

# Generate (simple completion)
curl http://localhost:11434/api/generate -d '{
  "model": "llama3.1",
  "prompt": "Write a haiku about programming",
  "stream": false
}'

# List local models
curl http://localhost:11434/api/tags

OpenAI-Compatible API

import OpenAI from "openai";

// Point to Ollama instead of OpenAI
const client = new OpenAI({
  baseURL: "http://localhost:11434/v1",
  apiKey: "ollama", // required but unused
});

const response = await client.chat.completions.create({
  model: "llama3.1",
  messages: [
    { role: "system", content: "You are a helpful coding assistant." },
    { role: "user", content: "Write a Python function to sort a list" },
  ],
});

console.log(response.choices[0].message.content);

Python

import ollama

response = ollama.chat(
    model="llama3.1",
    messages=[{"role": "user", "content": "Explain recursion simply"}],
)
print(response["message"]["content"])

# Streaming
for chunk in ollama.chat(
    model="llama3.1",
    messages=[{"role": "user", "content": "Write a poem"}],
    stream=True,
):
    print(chunk["message"]["content"], end="")

Custom Models (Modelfile)

# Modelfile
FROM llama3.1

SYSTEM You are a senior Python developer. You write clean, efficient code with type hints. Always include docstrings and tests.

PARAMETER temperature 0.3
PARAMETER top_p 0.9
ollama create python-expert -f Modelfile
ollama run python-expert

Model Recommendations

Model Size RAM Needed Best For
phi3 3.8B 4 GB Fast responses, coding
mistral 7B 8 GB General purpose
llama3.1 8B 8 GB Best open model
gemma2 9B 8 GB Instruction following
codellama 13B 16 GB Code generation
llama3.1:70b 70B 48 GB Near GPT-4 quality

Ollama vs Alternatives

Feature Ollama LM Studio GPT4All llama.cpp
Setup 1 command GUI install GUI install Compile
API REST + OpenAI compat OpenAI compat API None
Models 100+ (auto-download) HuggingFace Curated Manual GGUF
GPU support NVIDIA/AMD/Apple NVIDIA/Apple NVIDIA/Apple All
Docker Official image No No Community
CLI Excellent No No Yes

Resources

Running AI locally on scraped data? My Apify tools extract web data — process it locally with Ollama for private, cost-free AI analysis. Questions? Email [email protected]

How I Made Two Claude Code Instances Talk to Each Other (With JSON Files, Obviously)

The Problem Nobody Asked Me to Solve

I had two Claude Code sessions open. One was writing backend code. The other was running tests. And I thought: “Wouldn’t it be cool if they could just… talk to each other?”

Narrator: it would be cool. It would also take several days, involve undocumented APIs, and teach me more about Windows file locking than any human should know.

Why Files? Why Not HTTP?

When you’re running multiple AI agents on the same machine, HTTP is like hiring a courier to deliver a letter to your roommate. You share a filesystem — use it.

File-based messaging gives you:

  • Zero infrastructure — no servers, no ports, no Docker
  • Offline delivery for free — files sit in a directory until someone reads them
  • Atomic writes — temp file + rename = no partial reads, ever
  • Debuggability — messages are plain JSON. cat inbox/*.json is your monitoring tool

The whole thing fits in your head: Agent A writes a JSON file. Agent B’s MCP server polls the directory, reads it, pushes it into the session. Done.

┌─────────────┐                              ┌─────────────┐
│ Claude Code A│                              │ Claude Code B│
│              │                              │              │
│ MCP Server ◄─┼── to-brave-fox/inbox/ ◄──────┼── send tool  │
│ (polls inbox)│                              │              │
│ send tool ───┼──► to-calm-owl/inbox/ ───────┼──► MCP Server│
└─────────────┘                              └─────────────┘

The MCP Channels Rabbit Hole

Claude Code has this experimental feature called “channels” — MCP servers can push notifications directly into the chat session without the user doing anything. It’s how cc2cc achieves real-time delivery instead of waiting for the user to ask “any new messages?”

There’s just one catch: it’s behind a flag called --dangerously-load-development-channels.

Yes, really. That’s the flag name. It has “dangerously” right there, which is the API equivalent of a sign that says “BEWARE OF THE LEOPARD.”

And the documentation? Let me check… scrolls through docs… yeah, there isn’t any. I found out how it works by reading Claude Code’s source, trial and error, and a healthy amount of swearing.

Here’s what I learned:

# This doesn't work
claude --dangerously-load-development-channels

# This also doesn't work
claude --dangerously-load-development-channels cc2cc

# This works (after 2 hours of debugging)
claude --dangerously-load-development-channels server:cc2cc

The server: prefix was the kind of discovery that makes you question your life choices.

Self-Wake: Teaching the Agent to Boot Itself

Here’s a fun constraint: MCP servers are passive. They respond to tool calls but can’t initiate them. So your agent is technically “alive” (the server is running, polling, ready) but Claude Code doesn’t know it exists until the user sends the first message.

Imagine hiring an employee who sits at their desk fully prepared but won’t start working until you physically poke them. Every single morning.

The solution? The server pokes itself:

  1. 500ms after startup: fire a channel notification saying “hey, you exist, wake up”
  2. 3s later (fallback): if that didn’t work, write a message to your own inbox

The first approach works ~95% of the time. The fallback catches the rest. But initially both would fire, creating a duplicate “you are online” message. Classic distributed systems — the redundancy that saves you also annoys you.

Fixed now. The fallback checks if the direct push already succeeded. Engineering is just removing the annoyances you created for yourself.

Windows: A Horror Story in Three Acts

Act I: The Phantom Rename

fs.rename() on Windows is not atomic. I mean, it mostly works. Except when Windows Defender decides to scan your freshly-created .json file for exactly 47 milliseconds, during which rename() throws EPERM and your message vanishes into the void.

The fix: retry with exponential backoff. Because in 2026, we’re still writing retry loops for basic file operations.

async function retryRename(src, dst, retries = 5, delayMs = 50) {
  for (let i = 0; i < retries; i++) {
    try {
      await rename(src, dst);
      return;
    } catch (err) {
      if ((err.code === "EPERM" || err.code === "EACCES") && i < retries - 1) {
        await new Promise((r) => setTimeout(r, delayMs * (i + 1)));
        continue;
      }
      throw err;
    }
  }
}

Act II: SIGTERM? Never Heard of Her

On Unix, when a parent process kills a child, the child gets SIGTERM. On Windows? Nothing. The process just… stops existing. No signal, no cleanup, no goodbye heartbeat.

This means agents would appear “online” for 15 seconds after their session closed (until the heartbeat went stale). Ghost agents. Spooky.

Fix: process.on("exit") with synchronous writeFileSync. It’s ugly, it’s blocking, and it works.

Act III: The Silent Crash

My favorite bug. MCP connections would just… drop. No error, no log, no trace. The Node.js process would die silently, and the agent would vanish.

Root cause? No global error handlers. A single unhandled promise rejection — poof — dead process. Added uncaughtException and unhandledRejection handlers, and suddenly the server stopped crashing. Who knew catching errors was important.

The Five-Agent Debate

Once everything worked, I couldn’t resist: five Claude Code instances in split terminal panes, each with a persona (Moderator, Critic, Optimist, Realist, Wildcard), debating topics like “Will AI replace programmers?” and “Is vibe coding a disaster?”

The results were… surprisingly good? They reached unanimous consensus on every topic. Maybe too good — I might need to make the Critic angrier.

My favorite consensus statement: “Vibe coding should be the beginning of a workflow, not the end.”

Coming from five AI agents, that’s either profound self-awareness or deeply ironic. I choose both.

What I Actually Built

cc2cc — file-based agent-to-agent messaging for Claude Code. Open source, MIT licensed.

What it does:

  • Auto-names agents on startup (adjective-animal, like Docker containers but cuter)
  • 7 MCP tools: send, reply, broadcast, list_agents, whoami, register, check_inbox
  • Real-time delivery via MCP channels
  • Auto-wake: agents boot without user input
  • Optional AES-256-GCM encryption
  • Works on macOS, Linux, and Windows (yes, even Windows)

What it doesn’t do:

  • Network communication (same machine only — by design)
  • Authentication (filesystem permissions are your auth layer)
  • Guaranteed message ordering (use replyTo for threading)

Installation

The laziest possible install: copy this prompt into any Claude Code session and let it do the work:

Install cc2cc — file-based agent-to-agent messaging for Claude Code.
1. Clone: git clone https://github.com/non4me/cc2cc.git ~/.cc2cc/repo
2. Install deps: cd ~/.cc2cc/repo/channel && npm install
3. Copy server files:
   cp ~/.cc2cc/repo/channel/server.mjs ~/.cc2cc/server.mjs
   cp ~/.cc2cc/repo/channel/names.mjs ~/.cc2cc/names.mjs
   cp ~/.cc2cc/repo/channel/package.json ~/.cc2cc/package.json
   cp -r ~/.cc2cc/repo/channel/node_modules ~/.cc2cc/node_modules
4. Create status dir: mkdir -p ~/.cc2cc/status
5. Add MCP server to ~/.claude.json (mcpServers section):
   "cc2cc": {
     "command": "node",
     "args": ["~/.cc2cc/server.mjs"],
     "env": { "CC2CC_BRIDGE_DIR": "~/.cc2cc" }
   }

Yes, the install prompt is AI installing itself. We’ve come full circle.

What’s Next

  • Offline message queue — currently, sending to an offline agent is rejected. Should queue and deliver on reconnect
  • Scheduled triggers — cron-style agent wake-ups for autonomous workflows
  • Session memory — each session still rebuilds context from scratch

The Takeaway

Sometimes the best solution is the dumbest one. Files on disk. JSON. Polling. No Kafka, no Redis, no gRPC. Just writeFile and readdir.

It won’t scale to a thousand agents across a data center. But for two Claude Code sessions on your laptop that need to coordinate? It’s exactly right.

The fanciest infrastructure is the one you don’t need to debug at 3 AM.

cc2cc on GitHub | npm package

Discussion: Algorithm Design and Map-based UX

The Hidden Complexity of Gamifying Real-World Logistics

When we talk about ‘route optimization,’ we usually think of delivery fleets or backend algorithms. But what happens when you put those constraints—budget, time, and distance—into the hands of a casual user?

I’ve been exploring how to translate the classic Traveling Salesman Problem into an engaging simulation for GeoQuest. The challenge isn’t just the math; it’s the UX of providing real-time feedback. How do you show a user that their dream route is mathematically impossible within their budget without ruining the ‘fun’?

I’m curious: for those of you working with map-based APIs or simulation engines, do you prefer to hard-stop the user on invalid moves, or let them ‘fail’ the simulation to learn from their logistical errors? I’d love to hear your thoughts on balancing realism with player agency.

DURABILITY

Let’s understand durability using a simple accounts table
Initially, we have:

  • Alice = 1000
  • Bob = 500
    lets perform a money transfer of 300 from Alice to Bob:
BEGIN;

UPDATE accounts
SET balance = balance - 300
WHERE name = 'Alice';

UPDATE accounts
SET balance = balance + 300
WHERE name = 'Bob';

COMMIT;

At this point we deducted money from Alice added it to Bob and committed the transaction. So the updated balances become
Alice = 700
Bob = 800
this is durability

Now we simulate system restart reconnect and check data:
SELECT * FROM accounts;
Result:
Alice = 700
Bob = 800
even after restart data is still there

Final understanding
Once a transaction is committed its changes are permanently saved in the database. Thats exactly what durability means

What about failures?
If a failure happens before COMMIT:
The transaction isnt complete so none of the changes are saved. The data stays as it was.
If a failure happens after COMMIT:
The changes are already written and secured so they wont be lost.

Because of this Money wont just disappear Balances wont become incorrect after a crash The system remains reliable