DEV Community

Cover image for The Easiest Way to Build Auditable, Cryptographically-Secure Workflows.
Alya Mahalini
Alya Mahalini

Posted on

The Easiest Way to Build Auditable, Cryptographically-Secure Workflows.

πŸ›‘ Stop "Trusting." Start Proving.

A Look Under the Hood of Flowork's Crypto-Secure Automation. (This Ain't Your Grandma's Audit Log).

Let's paint a picture.

It's Monday morning. You're barely halfway through your coffee when a frantic message lands from the finance team. "Yo, that billing workflow from six months ago? It might've screwed up a huge batch of invoices. We need you to pull the exact logic that ran at 2:15 AM on October 27th. Like, now."

If you're using a typical cloud automation tool, this is the "oh crap" moment. You can probably find the current workflow. You might even have a "version history" that says, "Admin User updated this." But can you mathematically prove that the version you're looking at is the exact, unaltered code that executed? Can you prove it to a pissed-off auditor? What if a rogue admin (or a hacker) just... edited the logs?

This is the dirty little secret of most platforms: their audit logs are built on "trust me, bro." They're just entries in a database that can be changed. That's fine for fluff, but it's a nightmare for compliance, security, and proving you didn't mess up (called "non-repudiation").

But what if there was another way? What if, instead of a flimsy "trust me" log, you had a "prove it" cryptographic receipt for every... single... change... ever made?

That's the core idea we're about to rip apart. We're diving head-first into the source code of Flowork to show how it's built from the ground up to create workflows that aren't just "logged," but are cryptographically auditable and provably secure.

This isn't just a "better Zapier." This is a fundamentally different beast.


πŸ›οΈ The Foundation: Why a Hybrid Model is Your First Big Win

Before we even whisper the word "crypto," we gotta talk architecture.

Your typical automation-as-a-service (like Zapier or Make) is a black box. You hand over your sensitive dataβ€”customer info, API keys, the secret family recipeβ€”to their cloud, and they run it on their servers. You have zero control. You just... hope.

Flowork's model is hybrid. You get a slick, modern UI in the cloud to design your workflows, but the executionβ€”the actual workβ€”happens on a self-hosted "Core" engine that runs on your hardware (your laptop, a server, a Docker container).

This isn't a "nice-to-have" feature; it's a game-changer for data privacy. Don't believe me? Let's look at the code.

Code Deep Dive 1: The docker-compose.yml Blueprint

This is the architectural proof, not marketing fluff.

# A simplified look at C:\FLOWORK\docker-compose.yml
services:
  # The Gateway handles the connection to the cloud UI
  flowork_gateway:
    image: flowork/gateway:dev
   ...

  # The Core is YOUR engine. It does all the work.
  flowork_core:
    image: flowork/core:dev
   ...
    volumes:
      # This is the magic.
      # Your local folders are mounted directly into the engine.
      - ./data:/app/data
      - ./modules:/app/flowork_kernel/modules
      - ./plugins:/app/flowork_kernel/plugins
      - ./tools:/app/flowork_kernel/tools
      - ./ai_models:/app/flowork_kernel/ai_models
      - ./assets:/app/flowork_kernel/assets
   ...
volumes:
  flowork_data:
    driver: local
    driver_opts:
      device: './data' # Your database lives here, on your machine.
Enter fullscreen mode Exit fullscreen mode

Look at those volumes. Your database (./data), your custom code (./modules), and even your local AI models (./ai_models) are all mounted straight from your local filesystem.

When your workflow runs, it's not shipping your customer list off to a server in God-knows-where. It's processing customer.csv right off your hard drive. This is especially critical for AI. Running local, self-hosted AI models is the only way to use the power of LLMs without facing a "legal time bomb" of data privacy violations.

But the real genius? The flowork_core initiates an outbound WebSocket connection to the gateway. This means your engine can live behind a crazy-restrictive corporate firewall with zero open inbound ports.

You get the convenience of a cloud UI with the security of a paranoid, air-gapped network. Chef's kiss. πŸ’‹


πŸ” Pillar 1: Killing the Password with Crypto-Identity

Okay, so your data is safe on your machine. But what about you? Your identity?

Flowork's next move is to kill the password. Passwords suck. They get stolen, leaked, and phished.

Instead, your identity is a cryptographic keypair:

  • Your Private Key: A secret file (0x...) that lives only on your machine. This is your new password. You use it to "sign" messages, proving you are you.
  • Your Public Address: A public ID (0x...) made from your private key. This is your new username.

The server only ever knows your public address.

Code Deep Dive 2: The "Birth" of Your Key

When you set up Flowork, it doesn't just add a row to a users table. It forges a real cryptographic asset.

# A look at C:\FLOWORK\generate_env.py 

GUI_KEY_FILE_NAME = "DO_NOT_DELETE_private_key.txt"

def _gen_secret(length: int = 32) -> str:
    # 32 bytes = 64 hex chars = 256 bits
    return secrets.token_hex(length)

def write_gui_login_key(data_dir: Path, private_key: str):
    """
    (English Hardcode) Write the DO_NOT_DELETE_private_key.txt file for the GUI.
    (English Hardcode) This is read by the .bat scripts to show the user.
    """
    key_file_path = data_dir / GUI_KEY_FILE_NAME
    # ... content omitted for brevity ...
    key_file_path.write_text("\n".join(content), encoding="utf-8")

def main(argv):
    #... (setup code)...

    # Check if a key already exists
    if _should_rotate("ENGINE_OWNER_PRIVATE_KEY") and not gui_key_to_inject:
        print("[info] Generating new ENGINE_OWNER_PRIVATE_KEY.")
        # This is the "birth" of your key
        new_key = "0x" + _gen_secret(32) 
        new_env["ENGINE_OWNER_PRIVATE_KEY"] = new_key

    #... (more setup)...

    # This writes the key to the .txt file so you can find it
    write_gui_login_key(data_dir, new_env.get("ENGINE_OWNER_PRIVATE_KEY"))
Enter fullscreen mode Exit fullscreen mode

Code Deep Dive 3: Your Treasure Map

"Cool, a key. Where is it?" Flowork's run script is your friendly treasure map.

@echo off
rem A snippet from C:\FLOWORK\3-RUN_DOCKER.bat

echo --- MENCARI PRIVATE KEY ANDA... ---
echo.
echo    Your Login Private Key should appear below (inside the warning box):
echo.
set "KEY_FILE_PATH=%~dp0\data\DO_NOT_DELETE_private_key.txt"

if exist "%KEY_FILE_PATH%" (
    echo [INFO] Reading key from saved file: %KEY_FILE_PATH%
    echo.
    echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    echo !!! YOUR LOGIN PRIVATE KEY IS:
    echo.
    TYPE "%KEY_FILE_PATH%"
    echo.
    echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
) else (
    echo Key file not found at %KEY_FILE_PATH%
)
Enter fullscreen mode Exit fullscreen mode

You take this 0x... key and paste it into the login screen. But here's the magic: THAT KEY NEVER LEAVES YOUR BROWSER.

Code Deep Dive 4: The "Crypto Handshake"

Your browser doesn't send the key. It uses it to sign a challenge.

The Browser (Client-Side):

// A simplified look at C:\FLOWORK\flowork-gui\template\web\src\store\auth.js
import { ethers } from 'ethers';

async function loginWithPrivateKey(key) {
  try {
    // 1. Load the key into memory. It NEVER leaves the browser.
    const wallet = new ethers.Wallet(key);

    // 2. Get a one-time "challenge" from the server.
    const challenge = await apiGetLoginChallenge(wallet.address);

    // 3. Use the private key to SIGN the challenge.
    //    The key itself is NOT sent.
    const signature = await wallet.signMessage(challenge);

    // 4. Send your public ID, the challenge, and the signature.
    const profile = await apiGetProfile(wallet.address, challenge, signature);

    // You're in!

  } catch (error) {
    console.error("Login failed:", error);
  }
}
Enter fullscreen mode Exit fullscreen mode

The Server (Crypto-Firewall):
The server now plays "Guess Who?" with cryptography.

# A look at C:\FLOWORK\flowork-gateway\app\helpers.py
from eth_account.messages import encode_defunct
from web3.auto import w3

def _verify_signature(expected_address, message, signature):
    """
    (English Hardcode) Verify that the signature was created by the owner
    (English Hardcode) of the expected_address.
    """
    try:
        # 1. Re-create the exact same challenge message
        message_hash = encode_defunct(text=message)

        # 2. This is the magic. Recover the public address from ONLY
        #    the message and the signature.
        recovered_address = w3.eth.account.recover_message(
            message_hash, signature=signature
        )

        # 3. Check if the signer is who they say they are.
        if recovered_address.lower() == expected_address.lower():
            return True # Access Granted
    except Exception as e:
        current_app.logger.warning(f"Signature verification failed: {e}")

    return False # Access Denied
Enter fullscreen mode Exit fullscreen mode

This is the payoff: Your server's users table only has public addresses. An attacker who steals your entire gateway database gets... nothing. They have a list of public usernames, but they don't have the private keys. They can't log in. They can't sign anything.

A full database breach becomes a non-event for credential theft.


πŸ”— Pillar 2: The "Flowchain" (Your Unbreakable Alibi)

So, we've proven who you are. Now, let's prove what you did.

This is where Flowork's "Flowchain" comes in. When you save a workflow, you're not just overwriting a file or updating a row. You are committing a new, signed, and chained version to an append-only log.

Each workflow lives in its own folder (.../data/presets/My-Workflow/) and its history (v1_...json, v2_...json) acts as a mini-blockchain.

Code Deep Dive 5: Building the Chain

Let's look at the exact code that runs when you hit "Save."

# A simplified look at C:\FLOWORK\flowork-core\flowork_kernel\services\preset_manager_service\preset_manager_service.py
import json
import hashlib
import os

class PresetManagerService(BaseService):

    def get_latest_version_hash(self, workflow_dir: str) -> str | None:
        #... (finds the latest v_...json file and returns its hash)
        # For this example, let's say it returns "hash_of_v2"
        pass

    def _calculate_hash(self, file_path):
        #... (calculates the SHA256 hash of a file)
        pass

    def save_preset(self, preset_name: str, workflow_data: dict, 
                    author_id: str, signature: str):

        workflow_dir = os.path.join(self.presets_path, preset_name)
        os.makedirs(workflow_dir, exist_ok=True)

        # 1. Find the hash of the *previous* version (e.g., "hash_of_v2")
        previous_hash = self.get_latest_version_hash(workflow_dir)

        # 2. Get the next version number (e.g., 3)
        next_version = self.get_next_version_number(workflow_dir)
        new_version_path = os.path.join(workflow_dir, f"v{next_version}_...json")

        # 3. Create the new version "block"
        version_data = {
            "version": next_version,
            "timestamp": int(time.time()),
            "previous_hash": previous_hash,  # <-- CHAINS to v2
            "author_id": author_id,          # <-- PROVES who
            "signature": signature,          # <-- PROVES authenticity
            "workflow_data": workflow_data   # <-- The actual workflow
        }

        # 4. Save the new version file (v3_...json)
        with open(new_version_path, 'w', encoding='utf-8') as f:
            json.dump(version_data, f, indent=4)

        # 5. Calculate the hash of this new file (e.g., "hash_of_v3")
        current_hash = self._calculate_hash(new_version_path)

        # 6. Update the main preset file to point to this new "head"
        main_preset_file = os.path.join(workflow_dir, "preset.json")
        #... (save current_hash to main_preset_file)
Enter fullscreen mode Exit fullscreen mode

This is the whole system. When you save Version 3, its file contains the hash of Version 2. Version 2's file contains the hash of Version 1.

You've just built a chain of evidence.

Code Deep Dive 6: The Unblinking Auditor Bot

Now, how do you "audit" this? You run the verifier. This "Auditor Bot" script just walks the chain and checks the receipts.

# A look at C:\FLOWORK\flowork-core\flowork_kernel\utils\flowchain_verifier.py
import json
import os
import hashlib
from eth_account.messages import encode_defunct
from web3.auto import w3

#... (calculate_hash function is here)...

def verify_workflow_chain(workflow_directory):
    """
    Verifies the entire history chain of a workflow, from the
    newest version down to the first.
    """

    # 1. Get all version files (v1, v2, v3...) and sort them
    files = sorted(
        [f for f in os.listdir(workflow_directory) if f.endswith('.json') and f.startswith('v')],
        # ... (key omitted for brevity)
    )

    previous_file_hash = None

    # 2. Loop through the chain from v1 -> v2 -> v3...
    for i, filename in enumerate(files):
        file_path = os.path.join(workflow_directory, filename)
        with open(file_path, 'r', encoding='utf-8') as f:
            data = json.load(f)

        signature = data.get('signature')
        author_id = data.get('author_id')
        workflow_data = data.get('workflow_data')

        # === AUDIT CHECK #1: PROVE AUTHORSHIP ===
        # Re-create the data block that was signed
        unsigned_data_block = {"workflow_data": workflow_data}
        message_to_verify = json.dumps(unsigned_data_block, sort_keys=True, separators=(',', ':'))
        encoded_message = encode_defunct(text=message_to_verify)

        # Recover the signer's address from their signature
        recovered_address = w3.eth.account.recover_message(encoded_message, signature=signature)

        if recovered_address.lower() != author_id.lower():
            raise ValueError(f"Invalid signature in {filename}. Author mismatch!")

        # === AUDIT CHECK #2: PROVE INTEGRITY ===
        if i == 0: # This is v1, it should have no parent
            if data.get('previous_hash') is not None:
                raise ValueError(f"Chain broken at {filename}: First file has a previous_hash.")
        else: # This is v2 or later
            if data.get('previous_hash') != previous_file_hash:
                raise ValueError(f"Chain broken at {filename}! Hash mismatch.")

        # 3. The chain is valid so far. Store this file's hash
        #    to check it against the *next* file.
        previous_file_hash = calculate_hash(file_path)

    return True, "Chain verified."
Enter fullscreen mode Exit fullscreen mode

This is what "auditable" actually means.

  • Authorship Proof (Check #1): This proves who made the change. Because it's signed with their private key, it's non-repudiable. You can't say, "It wasn't me, my account was hacked." Yes, it was, and they used your key.
  • Integrity Proof (Check #2): This proves the history. If an attacker deletes v2_...json or changes one byte inside it, its hash will change. When the auditor checks v3_...json, its previous_hash (which stored the original hash of v2) will no longer match. The chain is instantly detected as broken.

πŸ›‘οΈ Pillar 3: The "Steel Fortress" That Protects the Auditor

Okay, final question for the big-brain hackers.

"If I can't modify the workflow files without breaking the chain... what if I just modify the auditor bot itself? What if I just edit flowchain_verifier.py to return True?"

Flowork thought of that. It's a service called the "Integrity Checker," nicknamed "Benteng Baja" (Steel Fortress).

Code Deep Dive 7: The App Audits Itself

Before Flowork even starts, this service runs and checks the integrity of all of its own core files.

# A look at C:\FLOWORK\flowork-core\flowork_kernel\services\integrity_checker_service\integrity_checker_service.py
import os
import json
import hashlib

class IntegrityCheckerService(BaseService):

    def __init__(self, kernel, service_id: str):
        super().__init__(kernel, service_id)
        # The true root is C:\FLOWORK\
        self.true_root_path = os.path.abspath(os.path.join(self.kernel.project_root_path, ".."))
        # This is the master list of "correct" file hashes
        self.core_manifest_path = os.path.join(self.true_root_path, "core_integrity.json")

    def _calculate_sha256(self, file_path):
        #... (calculates SHA-256 hash)...
        pass

    def verify_core_files(self):
        self.kernel.write_to_log("Benteng Baja: Verifying core file integrity...", "INFO")

        with open(self.core_manifest_path, "r", encoding="utf-8") as f:
            full_integrity_manifest = json.load(f)

        # Loop through EVERY file the app needs to run...
        for rel_path, expected_hash in full_integrity_manifest.items():

            # This includes "flowchain_verifier.py", "preset_manager_service.py", etc.
            full_path = os.path.join(self.true_root_path, rel_path.replace("/", os.sep))

            # Calculate the file's hash right now
            current_hash = self._calculate_sha256(full_path)

            if current_hash is None:
                raise RuntimeError(f"Integrity Check Failed: Core file '{rel_path}' is missing!")

            # Compare it to the "correct" hash
            if current_hash != expected_hash:
                # If they don't match, shut down the entire application.
                raise RuntimeError(f"Integrity Check Failed: Core file '{rel_path}' has been modified!")

        self.kernel.write_to_log(
             f"Benteng Baja: All {len(full_integrity_manifest)} files passed.", "SUCCESS"
        )
Enter fullscreen mode Exit fullscreen mode

This is the final checkmate.

  • An attacker can't modify the workflow history (Pillar 2) without the Auditor Bot catching it.
  • An attacker can't modify the Auditor Bot itself without the "Steel Fortress" (Pillar 3) catching that and refusing to even start the app.

🎀 The Verdict: Mic Drop.

What we've just walked through isn't a "feature list." It's a complete, end-to-end security philosophy built on cryptographic proof, not "trust me" promises.

Let's put it all together.

Feature Traditional Automation (Zapier, n8n) Flowork ("Flowchain" Model)
Identity System Email + Password. Vulnerable to DB breach. Private Key Signature. DB breach is a non-event.
Data Privacy Cloud-Only. All data processed on their servers. Secure Hybrid. Data never leaves your on-prem engine.
Audit Log Storage A mutable log in a cloud database. An append-only file chain on your local disk.
Proof of Change A simple string: "Admin updated this." A cryptographic signature from the user's private key.
Proof of History None. You must trust the log wasn't altered. Mathematical. The previous_hash chain proves history is intact.
Non-Repudiation No. An admin can claim, "My account was hacked." Yes. A user cannot deny a change signed by their key.
Third-Party Audit Impossible. Requires giving an auditor full admin. Trivial. Zip the workflow folder and email it. The auditor can verify it offline.

This is what it means to move from being a simple "connector" of apps to an "architect" of provable systems.

The "easiest way" to build auditable, cryptographically-secure workflows isn't a magic button. It's choosing a platform that was architected from line one to make "proof" a core part of the system, not an enterprise afterthought. The "easy" part is that Flowork handles all this complexity for you.

All you have to do is build.

https://github.com/flowork-dev/Visual-AI-Workflow-Automation-Platform

Top comments (0)