The Backyard Quarry, Part 2: Designing a Schema for Physical Objects

Diagram showing how a physical rock is represented as a digital record with metadata, images, and a 3D model.

In the first post of this series we set the stage for the Backyard Quarry project.

Once you decide every rock in the yard should have a record, the next question appears immediately:

What exactly should we record?

It’s a deceptively simple question. And like most simple questions in engineering, it opens the door to a surprisingly large number of decisions.

The First Attempt

The most straightforward approach is to keep things minimal.

Each rock gets an identifier and a few attributes.

Something like:

rock_id
size
price

At first glance, this seems reasonable.

We can identify the rock. We can describe it in some vague way. We can assign a price.

But this model breaks down almost immediately.

“Size” is ambiguous. Is that weight? Volume? Longest dimension? All of the above?

Two rocks of the same “size” might behave very differently when you try to move them.

And more importantly, this model doesn’t capture anything about the rock beyond its most basic characteristics.

It’s enough to sell a rock.

It’s not enough to understand one.

Expanding the Model

To make the system more useful, we need to be more explicit.

A slightly richer model might look like this:

rock_id
weight_lb
length_cm
width_cm
height_cm
color
rock_type
location_found
status

Now we’re getting somewhere.

We can distinguish between rocks that look similar but behave differently.

We can track where each rock came from.

We can start to answer questions like:

  • How many rocks do we have in a given area?
  • What size distribution does the dataset have?
  • Which rocks are suitable for different uses?

This is the point where the rock pile starts to feel less like a random collection and more like a dataset.

The Object Data Model

At a higher level, what we’re really doing is separating a physical object into a few distinct components.

Diagram showing how a physical rock is represented as a digital record with metadata, images, and a 3D model.
A simple model for representing a physical object as structured data and associated assets.

Each rock has:

  • metadata describing its properties
  • images representing its appearance
  • optionally, a 3D model capturing its shape

This separation turns out to be important.

Metadata is small, structured, and easy to query.

Images and 3D models are large, unstructured assets that need to be stored and referenced.

Keeping those concerns separate is a pattern that shows up in many real-world systems.

The Identity Problem

Once the schema starts to take shape, another question appears.

How do we uniquely identify a rock?

There are a few options:

  • sequential IDs (rock_001, rock_002)
  • UUIDs
  • physical tags attached to rocks
  • some form of image-based identification

For a small backyard dataset, almost anything works.

But the choice matters more as the system grows.

Sequential IDs are easy to read but require coordination.

UUIDs are globally unique but harder to work with manually.

Physical tags introduce a connection between the digital record and the real-world object.

Even in a simple system, identity becomes a design decision.

Classification: The Quarry Taxonomy

At some point, it becomes useful to introduce categories.

Originally this was just a convenience.

But like many things in this project, it quickly became something more formal.

A simple classification system might look like this:

Class 0 — Pebble
Class 1 — Hand Sample
Class 2 — Landscaping Rock
Class 3 — Wheelbarrow Class
Class 4 — Engine Block Class
Class 5 — Heavy Machinery Class

Each class roughly corresponds to how the rock is handled.

This turns out to be surprisingly useful.

Instead of asking for exact dimensions, we can filter by class:

  • “Show me all Pebble Class rocks”
  • “Exclude anything above Wheelbarrow Class”

In other words, we’ve introduced a derived attribute — something computed from the underlying data rather than stored arbitrarily.

This is exactly how classification systems evolve in real datasets.

Thinking About Lifecycle

Rocks don’t change much physically, but their role in the system does.

A rock might move through states like:

collected
cataloged
listed_for_sale
sold

Tracking this lifecycle introduces another dimension to the data.

Now we’re not just modeling objects.

We’re modeling *objects over *.

Even in a simple system, state and transitions begin to matter.

The Tradeoffs

At this point, the schema is already doing useful work.

But it’s also clear that there’s no perfect design.

Every decision involves tradeoffs:

  • more fields vs simplicity
  • normalized structure vs ease of use
  • flexibility vs consistency

The goal isn’t to design the perfect schema on the first try.

The goal is to design something that can evolve.

Because as soon as we start capturing real data, we’ll learn what we got wrong.

What Comes Next

With a basic schema in place, the next challenge becomes obvious.

We know what we want to store.

Now we need to figure out how to capture it.

In the next post, we’ll look at how to turn a physical rock into images, measurements, and potentially a 3D model — and how that process introduces its own set of constraints.

Because it turns out that collecting data from the physical world is rarely as clean as designing a schema on paper.

Facebooktwitterredditlinkedinmail

The post The Backyard Quarry, Part 2: Designing a Schema for Physical Objects appeared first on Blog of Ken W. Alger.

The Python Community’s Institutional Response to the Astral Acquisition Has Begun

The Python community has been waiting for an institutional answer to a simple question: when OpenAI acquired Astral and inherited control of python-build-standalone, would the PSF respond?

On March 23rd, a CPython core developer posted a quiet but significant update on discuss.python.org. The answer is yes.

What the Post Actually Said

Brett Cannon — long-time CPython core developer, former CPython release manager — posted this in the ongoing Astral acquisition discussion thread (post 49, March 23):

“I’m working on it (and have been since October). It’s been on pause waiting for Astral to upstream their patches after I gave them all an initial review at the end of January. I have a PEP that I’m about to go public with, and once that’s done I’m pivoting back to working on prebuilt, relocatable builds for CPython that will come from python.org.”

The GitHub repository — python/prebuilt-cpython — already exists.

A PEP is ready. The work has been ongoing since October 2025. This isn’t a proposal or a wish. The PSF’s official answer to Astral’s control of Python distribution infrastructure is in progress, and further along than anyone outside the core team knew.

Why This Matters: The python-build-standalone Problem

To understand what’s at stake, you need to know what python-build-standalone is.

python-build-standalone is a project that creates portable, self-contained CPython distributions — Python binaries that work without needing system Python, system libraries, or any external dependencies. You download the binary, it runs. This is what uv uses to install Python versions (the uv python install command). It’s what Rye uses. It’s the foundation that made self-contained Python tooling practical.

Before the OpenAI acquisition, python-build-standalone was controlled by Astral — which meant it was controlled by a well-funded, well-intentioned startup with strong community relationships. After the acquisition, it’s controlled by OpenAI, through Astral, which now belongs to a company whose core business is commercial AI.

This isn’t a catastrophe. OpenAI has given no indication of hostile intent toward Python infrastructure. But the concern is structural, not behavioral: a key piece of Python’s distribution infrastructure now sits under a commercial entity whose interests may not always align with the Python community’s.

The question developers have been asking since March 19: is there an official alternative? Is the PSF building a python-build-standalone equivalent that lives under their governance?

Brett Cannon’s post answers that. Yes. A PEP is coming. python/prebuilt-cpython exists. Python.org will eventually distribute official prebuilt, relocatable CPython builds.

What’s Actually Being Built

The scope is broader than just replacing python-build-standalone.

The python/prebuilt-cpython repo is building:

  • Relocatable CPython builds — move the directory anywhere, it still works (no hardcoded paths)
  • Python.org distribution — official source, not third-party
  • Cross-platform coverage — the same portability guarantees that make python-build-standalone valuable
  • Upstream from the PSF — governance sits with the foundation, not a corporate entity

The PEP will formalize this. Brett gave an initial technical review of Astral’s upstream patches in January 2026. Those patches being upstreamed unblocks the prebuilt-cpython work. The acquisition didn’t kill the upstream collaboration — if anything, Astral’s patches being in-flight is what makes the PSF’s own implementation possible now.

This is worth holding on to: the two tracks (Astral upstreaming patches, PSF building a python.org alternative) are not in conflict. They can both succeed.

What Changes (and What Doesn’t)

What changes: Python will eventually have official prebuilt, relocatable distributions from python.org. Tools that want to use PSF-governed Python binaries — not vendor-governed ones — will have that option. The dependency on python-build-standalone / Astral / OpenAI won’t be mandatory.

What doesn’t change yet:

  • uv still works exactly as before
  • ruff still works exactly as before
  • python-build-standalone still works exactly as before
  • OpenAI acquiring Astral still happened; that governance structure doesn’t change
  • The PEP hasn’t dropped yet — this is pre-announcement

What to watch: When the PEP goes public, it will be the first formal governance document addressing Python distribution independence in the post-acquisition world. It will be referenced in every subsequent discussion about Python toolchain lock-in. Read it.

The Bigger Picture

The Python community’s reaction to the Astral acquisition has been more nuanced than the initial HN threads suggested.

The strongest voices weren’t panicking — they were pattern-matching. “We’ve seen this before. Open-source infrastructure moves under corporate control, governance terms change, community forks happen, maintainers burn out, and two years later we’re migrating again.” Terraform → OpenTofu. Docker Desktop. Elasticsearch. The list is long.

What makes the Brett Cannon post significant isn’t just the technical content — it’s what it represents. A CPython core developer, working since October, building an official alternative, writing a PEP to formalize it. The PSF isn’t watching. They’re building.

The Python community’s institutional response isn’t a tweet thread or a GitHub issue or a strongly-worded blog post. It’s a PEP, a GitHub repo, and a core developer who’s been quietly working on this for five months.

That’s the actual story.

If You’re Building Tooling That Depends on Python Distribution

A few practical notes if this affects your work:

  1. Nothing breaks now. uv, ruff, python-build-standalone — all stable. No migration needed, no urgency.

  2. Watch for the PEP. When Brett’s PEP goes public, read it. It will clarify the intended governance structure, the timeline, and the transition path for tools that want to use python.org-governed Python distributions.

  3. The python/prebuilt-cpython repo is public now. If you want to track the technical work, it’s there.

  4. Community alternatives exist. If you need PSF-governed Python distribution alternatives while the PEP is in progress: python.org installers still work, pyenv works, system package managers work. Tools that need portability and zero-dependency installs don’t have official python.org alternatives yet — that’s what this work is building toward.

The toolchain anxiety is real and worth taking seriously. So is the institutional response.

Brett Cannon’s discuss.python.org post is in the “OpenAI to Acquire Astral” thread, post 49. The prebuilt-cpython work is at github.com/python/prebuilt-cpython.

If you’re building Python tooling and want practical patterns for testing it without depending on third-party subscription services, I write about that — Python Automation Cookbook covers patterns I reach for when building tools that have to work reliably without external service dependencies.

Banks Embrace Tokenized Deposits: The New Frontier of Onchain Cash

Banks Embrace Tokenized Deposits: The New Frontier of Onchain Cash

The financial landscape is undergoing a silent but seismic shift. As the
digital economy matures, traditional banking institutions are no longer
standing on the sidelines of the blockchain revolution. Instead, they are
aggressively pushing into the realm of tokenized deposits , viewing them
as the critical bridge between legacy banking systems and the future of
onchain finance.

What Are Tokenized Deposits?

At their core, tokenized deposits are a digital representation of a bank’s
liability to a customer, recorded on a programmable blockchain ledger. Unlike
stablecoins—which are typically issued by private entities like Circle or
Tether—tokenized deposits are issued by regulated financial institutions. They
represent a claim against the bank, functionally equivalent to a standard
demand deposit, but enhanced by the technological capabilities of distributed
ledger technology (DLT).

The Key Differences: Deposits vs. Stablecoins

  • Regulatory Oversight: Tokenized deposits fall under the umbrella of existing banking regulations and deposit insurance schemes.
  • Issuer Profile: Issued by licensed banks rather than non-bank fintech firms.
  • Settlement Mechanics: These assets are designed for instant, 24/7 settlement on permissioned or public blockchains, bypassing the delays inherent in traditional clearing houses like SWIFT or ACH.

Why Banks Are Racing Toward Onchain Cash

The race to innovate is driven by the urgent need to maintain relevance in a
world where “time is money.” In traditional markets, the settlement of a
transaction can take days. In an onchain ecosystem, settlement is virtually
instantaneous. For a bank, the implications of this shift are profound:

1. Efficiency Gains in Global Settlement

By using tokenized deposits, banks can facilitate cross-border payments with
near-zero latency. This reduces the need for the complex web of correspondent
banking relationships that currently plague international finance.

2. Programmability and Smart Contracts

Because these deposits exist on a blockchain, they can be infused with smart
contracts. This allows for automated conditional payments—such as escrow
services, automated invoice settlement, and complex trade finance
workflows—that execute only when predefined conditions are met.

3. The Liquidity Advantage

Tokenized deposits enable 24/7 liquidity. While traditional banks close their
settlement windows on weekends and holidays, onchain cash operates around the
clock, catering to the needs of modern institutional investors and
decentralized finance (DeFi) platforms.

The Competitive Landscape: Who Is Leading?

The movement is global. From JPMorgan’s Onyx platform to experiments by
Citigroup and various European banks, the appetite for “onchain cash” is
undeniable. Financial institutions are moving beyond pilot programs and are
now exploring how to integrate these assets into their core treasury
management services.

Addressing the Challenges

Despite the optimism, the transition to tokenized deposits is not without
hurdles:

  • Interoperability: Different banks are building on different ledgers. Creating a seamless bridge between these isolated chains is the next big engineering challenge.
  • Legal Certainty: While deposits are legally protected, the legal status of a “token” representing that deposit in a bankruptcy or insolvency scenario requires further clarification from regulators.
  • Privacy and Compliance: Balancing the transparency of a public blockchain with the strict KYC/AML requirements of the banking sector remains a delicate act.

The Future of Banking: Onchain and Programmable

As we look ahead, tokenized deposits appear to be the inevitable evolution of
money. By digitizing the deposit itself, banks are future-proofing their
balance sheets and ensuring they remain the foundation of the financial
system, even as that system migrates to DLT. The shift is not just about
technology; it is about reclaiming the efficiency lost in decades of
fragmented infrastructure.

Frequently Asked Questions

What is the difference between a CBDC and a tokenized deposit?

A Central Bank Digital Currency (CBDC) is a digital liability of the central
bank, whereas a tokenized deposit is a digital liability of a commercial bank.
They serve different roles in the broader financial architecture.

Are tokenized deposits safe?

Generally, yes. They are backed by the same bank balance sheets as traditional
deposits and typically fall under the same regulatory protections, such as
FDIC insurance in the United States.

How does this impact retail customers?

Initially, this shift will primarily impact institutional, wholesale, and
corporate finance. However, as the technology matures, retail banking apps may
eventually allow users to interact with tokenized deposits to facilitate
faster international transfers or interest-earning smart contract accounts.

Conclusion

The intensified focus on tokenized deposits signals a turning point for the
banking industry. By embracing onchain cash, banks are positioning themselves
to lead in the digital era, ensuring that the legacy systems of the past can
coexist with the programmable, decentralized finance of the future. The race
is on, and the institutions that master this technology will define the next
generation of financial services.

How to Setup Codacy: Complete Step-by-Step Guide (2026)

Codacy screenshot

What is Codacy and why use it?

Codacy is an automated code quality and security platform that analyzes your code for bugs, vulnerabilities, code smells, duplication, and complexity across 49 programming languages. It integrates directly with GitHub, GitLab, and Bitbucket to scan every pull request and provide actionable feedback before code reaches your main branch.

Instead of assembling a stack of separate tools for linting, SAST, SCA, secrets detection, and coverage tracking, Codacy bundles all of these into a single platform with a unified dashboard. For teams that want comprehensive code quality enforcement without managing multiple tools and CI configurations, this consolidation is the primary appeal.

This guide walks through every step of setting up Codacy from scratch – creating your account, connecting your repositories, configuring code patterns, enabling PR analysis, setting quality gates, uploading code coverage, writing a .codacy.yml configuration file, managing your team, and troubleshooting common issues. By the end, you will have a fully configured Codacy setup that scans every pull request automatically and enforces your team’s quality standards.

Codacy screenshot

Prerequisites

Before you begin the setup process, make sure you have the following ready:

  • A GitHub, GitLab, or Bitbucket account with admin access to the repositories you want to analyze
  • At least one repository with code in a supported language (Codacy supports 49 languages including JavaScript, TypeScript, Python, Java, Go, Ruby, PHP, C#, Kotlin, and Swift)
  • If you plan to upload code coverage, a CI pipeline (GitHub Actions, GitLab CI, CircleCI, Jenkins, or similar) that runs your test suite

The setup process itself takes under ten minutes for a single repository. Configuring code patterns, quality gates, and coverage reporting adds another 20-30 minutes depending on how much customization you need.

Step 1: Create your Codacy account

Navigate to app.codacy.com and click the sign-up button. Codacy supports sign-up through three Git providers:

  • GitHub – recommended if your repositories are on GitHub
  • GitLab – for GitLab cloud or self-managed instances
  • Bitbucket – for Bitbucket Cloud or Data Center

Select your Git provider and authorize Codacy to access your account. For GitHub, this involves two authorization steps:

  1. OAuth authorization – grants Codacy permission to read your profile and email address
  2. GitHub App installation – grants Codacy access to your repositories, webhooks, and pull request status checks

When installing the GitHub App, you can choose to grant access to all repositories in your organization or select specific repositories. If you are evaluating Codacy, start with one or two repositories rather than your entire organization.

After authorization, Codacy redirects you to your dashboard. If you signed up with a GitHub organization, Codacy automatically creates a Codacy organization that mirrors your GitHub org structure.

Step 2: Connect and add your first repository

From your Codacy dashboard, click the “Add repository” button in the top navigation. Codacy displays a list of all repositories it has access to, filtered by your Git provider organization.

Select the repository you want to add and click “Add.” Codacy immediately begins the initial analysis, which involves:

  1. Cloning the repository (Codacy accesses the code temporarily for analysis and does not store your source code permanently)
  2. Detecting the programming languages present in the repository
  3. Running the default analysis tools for each detected language
  4. Generating an initial quality report

The first analysis typically takes between 2 and 15 minutes depending on the repository size. You can monitor the progress from the repository dashboard page.

Once the initial analysis completes, Codacy displays your repository’s quality metrics:

  • Grade – an overall letter grade (A through F) based on the aggregated quality metrics
  • Issues – the total number of issues found, categorized by severity (error, warning, info) and type (security, error-prone, performance, code style, compatibility, unused code)
  • Complexity – file-level and function-level complexity scores
  • Duplication – the percentage of duplicated code blocks
  • Coverage – code coverage percentage (requires separate setup, covered in Step 7)

Step 3: Understand the Codacy dashboard

The repository dashboard is your central hub for monitoring code quality over time. It is worth spending a few minutes understanding its layout before diving into configuration.

The dashboard displays several key panels:

Quality evolution chart shows how your quality grade and issue count have changed over the past days, weeks, or months. This is useful for tracking whether your team’s code quality is improving or degrading over time.

Open issues panel lists all currently open issues grouped by category. You can filter by severity, category, language, and analysis tool to focus on what matters most. Each issue links to the specific file and line number where it was found.

Pull request analysis panel shows recent pull requests with their quality status – whether they passed or failed your quality gates, how many new issues were introduced, and what the coverage delta was.

File quality panel ranks your files by the number of issues, complexity, and coverage. This is useful for identifying the most problematic files in your codebase and prioritizing cleanup work.

Step 4: Configure code patterns

Code patterns are the rules that Codacy uses to analyze your code. Each supported language has one or more analysis tools, and each tool has a set of rules (called patterns) that can be individually enabled or disabled.

To configure code patterns:

  1. Navigate to your repository in Codacy
  2. Click Code patterns in the left sidebar
  3. You will see a list of analysis tools grouped by language

For a JavaScript/TypeScript repository, you might see tools like ESLint, JSHint, and Codacy’s own analyzers. For Python, you might see Pylint, Bandit, Prospector, and Ruff. For Java, you might see PMD, SpotBugs, and Checkstyle.

Enabling and disabling tools

Each analysis tool has a toggle switch. If you already use a specific linter locally (for example, ESLint with a custom configuration), you may want to disable Codacy’s built-in ESLint to avoid conflicts. Alternatively, you can configure Codacy to use your repository’s existing ESLint configuration file.

Configuring individual patterns

Click on any tool to expand its rule list. Each rule shows:

  • Rule name and description – what the rule checks for
  • Severity – error, warning, or info
  • Category – security, error-prone, performance, code style, compatibility, or unused code
  • Toggle – enable or disable the rule

For a new setup, the recommended approach is to start with the default patterns enabled and then tune them based on your first analysis results. If the initial analysis produces too many low-value findings, disable the noisiest patterns first (typically code style patterns for legacy codebases).

Using your existing linter configuration

If your repository already has a linter configuration file (.eslintrc.js, pylintrc, .rubocop.yml, etc.), you can configure Codacy to use it instead of Codacy’s default patterns:

  1. Go to Code patterns and select the relevant tool
  2. Enable the “Use the configuration file in my repository” toggle
  3. Codacy will use your existing configuration file for that tool

This is particularly useful for teams that have already invested time in customizing their linter rules and do not want to duplicate that work in the Codacy UI.

Step 5: Setup PR analysis

Pull request analysis is one of Codacy’s most valuable features because it catches issues before they reach your main branch. When configured, Codacy automatically analyzes every pull request and reports:

  • New issues introduced by the PR
  • Issues that were fixed by the PR
  • Coverage changes (increase or decrease)
  • Complexity changes
  • Whether the PR passes or fails your quality gates

How PR analysis works

When you open a pull request on a connected repository, Codacy’s GitHub App receives a webhook notification. It then:

  1. Analyzes only the files changed in the PR (not the entire repository)
  2. Compares the results against the target branch to identify new versus existing issues
  3. Posts a status check on the PR (pass or fail based on quality gates)
  4. Optionally posts a summary comment on the PR with detailed findings

PR analysis is enabled by default when you connect a repository through the GitHub App. To verify it is working, open a pull request on your connected repository and check for the Codacy status check.

Configuring PR status checks

To customize what Codacy reports on your pull requests:

  1. Navigate to your repository in Codacy
  2. Click Settings in the left sidebar
  3. Scroll to the Pull Requests section

Here you can configure:

  • Status check behavior – whether to post a single combined status check or separate checks for issues, coverage, and duplication
  • PR comment – whether to post a summary comment on the PR, and what information to include
  • Annotations – whether to add inline annotations on specific lines in the PR diff

Making Codacy status checks required

To prevent pull requests from being merged when they fail Codacy’s quality gates, configure branch protection rules in your Git provider:

For GitHub:

  1. Go to your repository settings on GitHub
  2. Click Branches in the sidebar
  3. Click Add branch protection rule (or edit an existing rule for your main branch)
  4. Under “Require status checks to pass before merging,” search for and select the Codacy status checks
  5. Save the rule
Branch name pattern: main

[x] Require status checks to pass before merging
    Required status checks:
      - Codacy Static Code Analysis

For GitLab:

  1. Go to your project settings
  2. Navigate to Merge Requests
  3. Under “Merge checks,” enable “Pipelines must succeed”
  4. Codacy reports its results as a pipeline job via the GitLab integration

Step 6: Configure quality gates

Quality gates define the minimum quality standards that your code must meet. They apply both at the repository level (tracking overall quality trends) and at the pull request level (blocking merges that degrade quality).

To configure quality gates:

  1. Navigate to your repository in Codacy
  2. Click Quality settings in the left sidebar
  3. Set thresholds for each metric

Available quality gate metrics

Metric Description Recommended Starting Threshold
New issues Maximum number of new issues allowed in a PR 0 critical, 5 total
Coverage Minimum coverage on new code 60% (adjust based on your current baseline)
Coverage variation Maximum allowed coverage decrease -1%
Duplication Maximum duplication in new code 3%
Complexity Maximum file complexity allowed No threshold initially

Setting practical thresholds

The key to effective quality gates is starting with thresholds your team can actually meet. If your current code coverage is 40%, setting a quality gate at 80% coverage will cause every PR to fail and your team will quickly disable the gate.

A practical rollout strategy:

  1. Week 1-2: Set quality gates to observe mode – track but do not block. Look at how your PRs perform against the default thresholds
  2. Week 3-4: Enable blocking quality gates with achievable thresholds – zero new critical issues and coverage must not decrease
  3. Month 2+: Gradually tighten thresholds as your team adapts – increase minimum coverage, reduce allowed new issues

Repository-level quality gates

In addition to PR-level gates, Codacy tracks quality at the repository level. The repository quality gate evaluates the overall codebase against thresholds you set for total issues, overall coverage, and overall duplication. This is useful for tracking long-term quality trends and setting goals for codebase cleanup.

Step 7: Setup code coverage with the Coverage Reporter

Code coverage tracking in Codacy requires uploading coverage reports from your CI pipeline. Codacy does not run your tests – it processes coverage reports generated by your existing test suite and displays the results alongside other quality metrics.

Step 7a: Generate a coverage report

First, make sure your test suite generates a coverage report in one of the supported formats:

  • LCOV – common for JavaScript/TypeScript (Istanbul, c8, nyc)
  • Cobertura XML – common for Python (pytest-cov, coverage.py) and Java (JaCoCo with Cobertura output)
  • JaCoCo – standard for Java/Kotlin
  • Clover – used by PHP (PHPUnit) and some Java projects
  • OpenCover – common for .NET projects

For example, in a JavaScript project using Vitest:

{
  "test": {
    "coverage": {
      "provider": "v8",
      "reporter": ["lcov", "text"]
    }
  }
}

Or in a Python project using pytest:

pytest --cov=src --cov-report=xml:coverage.xml

Step 7b: Get your project API token

  1. Navigate to your repository in Codacy
  2. Click Settings in the left sidebar
  3. Scroll to the Coverage section (or look under Integrations)
  4. Copy the Project API Token – you will need this to authenticate the upload

Store this token as a secret in your CI environment:

  • GitHub Actions: Add it as a repository secret named CODACY_PROJECT_TOKEN
  • GitLab CI: Add it as a CI/CD variable named CODACY_PROJECT_TOKEN
  • CircleCI: Add it as an environment variable

Step 7c: Add the Coverage Reporter to your CI pipeline

For GitHub Actions:

name: Tests and Coverage

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci

      - name: Run tests with coverage
        run: npm test -- --coverage

      - name: Upload coverage to Codacy
        uses: codacy/codacy-coverage-reporter-action@v1
        with:
          project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
          coverage-reports: coverage/lcov.info

For GitLab CI:

test:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm test -- --coverage
  after_script:
    - bash <(curl -Ls https://coverage.codacy.com/get.sh) report
        -r coverage/lcov.info
        --project-token $CODACY_PROJECT_TOKEN
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

For Python projects with GitHub Actions:

name: Tests and Coverage

on:
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install -r requirements.txt pytest pytest-cov

      - name: Run tests with coverage
        run: pytest --cov=src --cov-report=xml:coverage.xml

      - name: Upload coverage to Codacy
        uses: codacy/codacy-coverage-reporter-action@v1
        with:
          project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
          coverage-reports: coverage.xml

Using the standalone CLI reporter:

If you prefer not to use the GitHub Action, you can use the Coverage Reporter CLI directly:

# Download the reporter
curl -Ls https://coverage.codacy.com/get.sh -o codacy-coverage-reporter.sh

# Upload the report
bash codacy-coverage-reporter.sh report 
  -r coverage/lcov.info 
  --project-token $CODACY_PROJECT_TOKEN

Uploading multiple coverage reports

If your project has multiple test suites or generates coverage reports for different components (common in monorepos), you can upload them as partial reports and finalize them:

# Upload first report as partial
bash <(curl -Ls https://coverage.codacy.com/get.sh) report 
  -r coverage/unit.lcov.info 
  --partial 
  --project-token $CODACY_PROJECT_TOKEN

# Upload second report as partial
bash <(curl -Ls https://coverage.codacy.com/get.sh) report 
  -r coverage/integration.lcov.info 
  --partial 
  --project-token $CODACY_PROJECT_TOKEN

# Finalize the coverage
bash <(curl -Ls https://coverage.codacy.com/get.sh) final 
  --project-token $CODACY_PROJECT_TOKEN

Once coverage is uploaded, Codacy displays coverage metrics on your repository dashboard and includes coverage changes in every PR analysis.

Step 8: Create a .codacy.yml configuration file

The .codacy.yml file gives you version-controlled configuration for your Codacy analysis. Instead of configuring everything through the Codacy UI, you can define your analysis settings in a file that lives in your repository root – making the configuration reviewable, consistent, and part of your standard code review process.

Basic .codacy.yml structure

Create a file named .codacy.yml in your repository root:

---
engines:
  eslint:
    enabled: true
    config: .eslintrc.js
  stylelint:
    enabled: true
  csslint:
    enabled: false
  duplication:
    enabled: true
    config:
      minimum_tokens: 50
  metrics:
    enabled: true

languages:
  javascript:
    extensions:
      - .js
      - .jsx
      - .mjs
  typescript:
    extensions:
      - .ts
      - .tsx

exclude_paths:
  - "node_modules/**"
  - "dist/**"
  - "build/**"
  - "coverage/**"
  - "**/*.min.js"
  - "**/*.bundle.js"
  - "vendor/**"
  - "public/assets/**"
  - ".next/**"

Enabling tools with custom configuration files

If your repository already has linter configuration files, reference them in .codacy.yml:

---
engines:
  eslint:
    enabled: true
    config: .eslintrc.json
  pylint:
    enabled: true
    config: .pylintrc
  rubocop:
    enabled: true
    config: .rubocop.yml
  checkstyle:
    enabled: true
    config: checkstyle.xml

When you specify a config path, Codacy uses your repository’s linter configuration instead of its default patterns. This ensures that Codacy’s analysis matches what developers see when running linters locally.

Complete .codacy.yml example for a full-stack project

Here is a comprehensive configuration for a typical full-stack project with JavaScript, Python, and Docker:

---
engines:
  eslint:
    enabled: true
    config: .eslintrc.json
  pylint:
    enabled: true
    config: .pylintrc
  bandit:
    enabled: true
  prospector:
    enabled: false
  duplication:
    enabled: true
    config:
      minimum_tokens: 70
  metrics:
    enabled: true
  hadolint:
    enabled: true
  shellcheck:
    enabled: true
  markdownlint:
    enabled: false

languages:
  javascript:
    extensions:
      - .js
      - .jsx
      - .mjs
  typescript:
    extensions:
      - .ts
      - .tsx
  python:
    extensions:
      - .py

exclude_paths:
  - "node_modules/**"
  - "dist/**"
  - "build/**"
  - "coverage/**"
  - "**/*.min.js"
  - "**/*.bundle.js"
  - "**/*.generated.*"
  - "vendor/**"
  - "migrations/**"
  - "static/vendor/**"
  - "tests/fixtures/**"
  - "**/__snapshots__/**"
  - ".github/**"
  - "docs/**"

When to use .codacy.yml vs. the UI

Use .codacy.yml when:

  • You want configuration to be version-controlled and reviewable in PRs
  • Multiple team members need to adjust analysis settings
  • You have multiple repositories and want consistent configuration
  • You want to reference existing linter configuration files

Use the Codacy UI when:

  • You are evaluating Codacy and experimenting with different settings
  • You need to quickly toggle rules without committing code
  • You want to use Codacy’s visual rule browser to explore available patterns

Note that .codacy.yml settings override UI settings when both are present. If you define an engine in .codacy.yml, the UI configuration for that engine is ignored.

Step 9: Team management and permissions

Managing team access in Codacy follows your Git provider’s organization structure. When you connect an organization from GitHub, GitLab, or Bitbucket, Codacy mirrors the membership automatically.

How team syncing works

Codacy pulls team membership from your Git provider. When a developer joins your GitHub organization and has access to connected repositories, they automatically appear in Codacy. When someone leaves the organization, their access is revoked.

You do not need to manually invite every team member. However, you can invite members directly from Codacy if they have not yet logged in:

  1. Go to your Organization settings in Codacy
  2. Click People in the sidebar
  3. Click Add people and enter their email address or Git provider username

Role-based access control

Codacy provides three permission levels:

Role Permissions
Admin Full control – manage organization settings, billing, integrations, add/remove repositories, configure all tools and quality gates
Manager Repository management – configure code patterns, quality gates, and PR settings for repositories they have access to. Cannot manage billing or organization settings
Developer View-only for configuration – can view dashboards, issues, and analysis results. Cannot change code patterns, quality gates, or repository settings

To change a team member’s role:

  1. Go to Organization settings and click People
  2. Find the member in the list
  3. Click the role dropdown and select the new role

Managing repository access

By default, Codacy syncs repository access from your Git provider. If a developer has read access to a repository on GitHub, they can view that repository’s analysis in Codacy. If they have write access, they can configure analysis settings (depending on their Codacy role).

You can also restrict which repositories are visible in Codacy by adjusting the GitHub App permissions:

  1. Go to your GitHub organization settings
  2. Click GitHub Apps in the sidebar
  3. Click Configure next to the Codacy app
  4. Under “Repository access,” select specific repositories instead of “All repositories”

Step 10: Ignoring files and directories

Not every file in your repository needs analysis. Generated code, vendored dependencies, build outputs, test fixtures, and third-party assets will produce noise that clutters your dashboard and obscures real issues.

Method 1: Using the Codacy UI

  1. Navigate to your repository in Codacy
  2. Click Settings in the left sidebar
  3. Click Ignored files
  4. Add glob patterns for files and directories to exclude

Common patterns to add:

**/node_modules/**
**/vendor/**
**/dist/**
**/build/**
**/.next/**
**/coverage/**
**/*.min.js
**/*.min.css
**/*.bundle.js
**/*.generated.*
**/migrations/**
**/fixtures/**
**/__snapshots__/**
**/*.lock
**/package-lock.json
**/yarn.lock

Method 2: Using .codacy.yml

Add an exclude_paths section to your .codacy.yml file (as shown in Step 8). This method is preferred because the exclusions are version-controlled.

Method 3: Tool-specific ignores

Some analysis tools respect their own ignore files. For example:

  • ESLint reads .eslintignore
  • Pylint reads ignore patterns from .pylintrc
  • Bandit reads .bandit configuration

If you configure Codacy to use your repository’s linter configuration files, these tool-specific ignore patterns are respected automatically.

Step 11: Advanced configuration

Setting up Codacy with GitHub Actions as a standalone check

While Codacy’s GitHub App handles most analysis automatically, you can also run the Codacy CLI in your own CI pipeline for more control:

name: Codacy Analysis

on:
  pull_request:
    branches: [main]

jobs:
  codacy-analysis:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Codacy Analysis CLI
        uses: codacy/codacy-analysis-cli-action@master
        with:
          project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
          upload: true
          max-allowed-issues: 0

This approach is useful when you want Codacy analysis to run as part of your existing CI pipeline rather than as a separate webhook-triggered process.

Configuring the Codacy AI Reviewer

Codacy’s AI Reviewer uses large language models to provide contextual feedback on pull requests beyond what static analysis rules can catch. To configure it:

  1. Navigate to your repository in Codacy
  2. Click Settings in the left sidebar
  3. Look for the AI Reviewer or AI features section
  4. Enable the AI Reviewer toggle

The AI Reviewer posts additional comments on your PRs that cover logic issues, potential bugs, and improvement suggestions that static rules would not catch. Note that the AI Reviewer is available on paid plans (Team and Business).

Configuring SAST and secrets detection

Codacy includes built-in SAST (Static Application Security Testing) and secrets detection:

SAST scanning is enabled by default and uses security-focused rules from tools like Bandit (Python), SpotBugs with FindSecBugs (Java), and Codacy’s own security analyzers. Security issues appear in the Issues panel with a “Security” category tag.

Secrets detection scans for hardcoded credentials, API keys, and tokens. It is enabled at the organization level:

  1. Go to Organization settings
  2. Click Security or Integrations
  3. Enable Secrets detection

When Codacy detects a potential secret in a pull request, it flags it as a critical security issue and blocks the PR if your quality gates are configured to block on critical issues.

Troubleshooting common issues

Even with a straightforward setup, you may encounter issues. Here are the most common problems and their solutions.

Codacy is not analyzing my pull requests

Check the GitHub App permissions. Go to your GitHub organization settings, click “GitHub Apps,” and verify that the Codacy app has access to the repository in question. If you recently changed the app’s repository access, Codacy may need a webhook re-delivery.

Check the repository status. In Codacy, navigate to your repository and look at the status indicator. If analysis is paused or errored, click into the status for details. Common causes include rate limiting (too many concurrent analyses) or a misconfigured default branch.

Verify webhook delivery. Go to your GitHub repository settings, click “Webhooks,” find the Codacy webhook, and check the “Recent Deliveries” tab. Look for failed deliveries (non-200 status codes) and redeliver them.

Coverage is not showing up

Verify the upload succeeded. Check your CI pipeline logs for the Codacy Coverage Reporter output. A successful upload prints a confirmation message with the commit SHA. If the upload fails, the most common causes are an incorrect project token or a malformed coverage report.

Check the commit SHA. Codacy matches coverage reports to specific commits. If your CI pipeline runs on a merge commit (common with GitHub Actions pull_request events), the SHA may not match the PR head. Use the --commit-uuid flag to specify the correct SHA:

bash <(curl -Ls https://coverage.codacy.com/get.sh) report 
  -r coverage/lcov.info 
  --commit-uuid ${{ github.event.pull_request.head.sha }} 
  --project-token $CODACY_PROJECT_TOKEN

Check the coverage report format. Codacy supports LCOV, Cobertura, JaCoCo, Clover, and OpenCover. If your report is in a non-standard format or is malformed, the upload may succeed but Codacy will not process it. Validate your coverage report locally before uploading.

Too many false positives

Disable noisy tools. If a specific analysis tool produces mostly irrelevant findings for your codebase, disable it in Code patterns. You can always re-enable it later after addressing higher-priority issues.

Use your own configuration files. Enable the “Use configuration file in repository” option for tools that support it. Your team’s existing linter configuration reflects patterns that have already been tuned for your codebase.

Exclude generated and vendored code. Files that are not authored by your team (generated code, vendored dependencies, migration files) should be excluded from analysis. See Step 10 for exclusion methods.

Start with high-severity rules only. In Code patterns, sort rules by severity and disable info-level and some warning-level rules initially. Focus on error-level rules that catch actual bugs and security vulnerabilities.

Analysis is slow

Check your repository size. Codacy’s initial analysis processes your entire repository, which takes longer for large codebases. Subsequent PR analyses only process changed files and should be significantly faster.

Reduce the number of enabled tools. Each analysis tool adds processing time. If you have ten tools enabled but only use three languages, disable the tools for languages not present in your repository.

Exclude large directories. Build output directories, vendored dependencies, and asset directories add files to the analysis queue without providing value. Exclude them using .codacy.yml or the Codacy UI.

Quality gate is too strict or too lenient

Review the gate thresholds. Navigate to Quality settings and check each threshold. If every PR is failing, your thresholds may be unrealistic for the current state of your codebase. If no PR ever fails, your thresholds are too lenient.

Use the “new code” focus. Configure quality gates to evaluate only new code introduced in the PR rather than the entire repository. This prevents legacy code issues from blocking new work and lets your team maintain quality standards going forward without requiring a full codebase cleanup first.

Best practices for ongoing Codacy usage

After the initial setup, maintaining your Codacy configuration requires periodic attention to keep it effective.

Review new issues weekly. Assign a rotating team member to triage new Codacy findings each week. Dismiss false positives with a reason, assign real issues to the appropriate developer, and adjust patterns when a rule consistently produces unhelpful feedback.

Update quality gate thresholds quarterly. As your codebase improves, tighten the thresholds to maintain pressure for quality improvement. If coverage started at 40% when you onboarded Codacy and has risen to 55%, update the minimum coverage threshold to 50% so it continues to serve as a meaningful gate.

Keep your .codacy.yml in sync with your linter configs. When you update your ESLint configuration locally, make sure Codacy is set to use the repository’s configuration file rather than its own copy. This prevents drift between what developers see locally and what Codacy reports on PRs.

Use Codacy’s organization-level settings for consistency. If you have multiple repositories, configure shared settings (default quality gates, default tool configurations, default exclusion patterns) at the organization level. Individual repositories can override these defaults when needed, but starting from a shared baseline keeps things consistent.

Monitor the Codacy status page. Codacy occasionally experiences service disruptions that affect analysis processing times. Check status.codacy.com if analyses are taking unusually long or not completing.

Summary

Setting up Codacy involves ten main steps: creating your account, connecting a repository, understanding the dashboard, configuring code patterns, enabling PR analysis, setting quality gates, uploading code coverage, creating a .codacy.yml configuration file, managing team permissions, and configuring file exclusions. The initial setup takes under 30 minutes for a single repository, and the ongoing maintenance is minimal once you have tuned the analysis to match your team’s standards.

Codacy’s biggest advantage is consolidation – it replaces the need for separate tools for linting, SAST, SCA, secrets detection, coverage tracking, and duplication analysis. For teams that want a single dashboard for code quality without assembling and maintaining a multi-tool stack, Codacy provides a compelling solution at a reasonable price point.

For more details on Codacy’s pricing tiers and how it compares to alternatives, see our Codacy pricing breakdown and Codacy alternatives guide.

Frequently Asked Questions

How do I setup Codacy for my GitHub repository?

Sign up at app.codacy.com using your GitHub account, authorize the Codacy GitHub App, select the repositories you want to analyze, and Codacy will automatically run its first analysis. The entire process takes under five minutes. Once connected, Codacy scans every new pull request and commits to your default branch automatically.

Is Codacy free to use?

Yes, Codacy offers a free Developer plan that includes the AI Guardrails IDE extension for VS Code, IntelliJ, Cursor, and Windsurf. For team features like PR analysis, quality gates, dashboards, and code coverage tracking, you need the Team plan at $18/dev/month (annual billing). Open-source projects get the full Team plan for free.

How do I connect Codacy to GitHub?

Go to app.codacy.com and click Sign up with GitHub. Authorize the Codacy OAuth app to read your profile, then install the Codacy GitHub App on your organization or personal account. Select the repositories you want Codacy to access. Codacy creates webhooks automatically so that every push and pull request triggers analysis.

How do I configure Codacy code patterns?

Navigate to your repository in Codacy, click Code patterns in the left sidebar, and you will see all available analysis tools listed by language. Each tool shows its individual rules which you can toggle on or off. You can also switch entire tools on or off. For bulk configuration, use a .codacy.yml file in your repository root to define which tools and rules to enable.

How do I setup Codacy PR analysis?

PR analysis is enabled by default when you connect a repository through the GitHub App integration. Codacy automatically posts a status check and summary comment on every pull request showing new issues, coverage changes, and quality gate status. You can customize what appears in the PR comment from your repository’s Settings page under Pull Requests.

How do I add code coverage to Codacy?

Install the Codacy Coverage Reporter in your CI pipeline using the bash script or CLI binary. Generate a coverage report in your test suite (LCOV, Cobertura, JaCoCo, or Clover format), then upload it with the codacy-coverage-reporter tool using your project API token. Codacy will display coverage metrics on your dashboard and in PR comments.

What is the .codacy.yml file used for?

The .codacy.yml file is a repository-level configuration file that lets you define which analysis tools and patterns are enabled, exclude files and directories from analysis, set language-specific overrides, and configure coverage settings. It lives in your repository root and overrides the Codacy UI settings, making configuration version-controlled and consistent across your team.

How do I ignore files in Codacy?

You can ignore files in Codacy through two methods. In the UI, go to your repository Settings, click Ignored files, and add glob patterns like /test/, /vendor/, or */.min.js. Alternatively, add an exclude_paths section to your .codacy.yml file. Both methods prevent Codacy from analyzing matching files and directories.

How do I setup Codacy quality gates?

Go to your repository in Codacy, click Quality settings in the sidebar, then configure thresholds for issues (new and total), complexity, duplication, and code coverage. Quality gates apply to both the overall repository and individual pull requests. When a PR fails a quality gate, Codacy reports a failing status check on the PR, which you can configure to block merges through branch protection rules.

Does Codacy work with GitLab and Bitbucket?

Yes, Codacy supports GitHub, GitLab (cloud and self-managed), and Bitbucket (cloud and Data Center). The setup process is similar across all platforms – sign up with your Git provider account, authorize the integration, and select repositories. PR analysis, quality gates, and coverage tracking work on all three platforms.

How do I add team members to Codacy?

Team members are added automatically when they join your Git provider organization and have access to repositories connected to Codacy. You can also invite members manually from the Organization settings page in Codacy. Role-based access control lets you assign Admin, Manager, or Developer roles with different permission levels for configuring tools, managing repositories, and viewing dashboards.

Why is Codacy not analyzing my repository?

The most common causes are: the repository language is not supported, the GitHub App does not have permission to access the repository, the repository is empty or has no analyzable files, or analysis is paused. Check the repository status page in Codacy for error messages. If the GitHub App permissions were recently changed, you may need to re-authorize from your GitHub organization settings.

How long does Codacy take to analyze a repository?

The initial full analysis typically takes 2-15 minutes depending on repository size, number of files, and languages used. Subsequent PR analyses are faster because Codacy only scans changed files, usually completing in 1-3 minutes. Very large repositories with hundreds of thousands of lines may take longer on the first scan.

Can I use Codacy with monorepos?

Yes, Codacy supports monorepos. You can configure different analysis tools and patterns for different directories within the same repository using the .codacy.yml file. File exclusion patterns let you scope analysis to specific services or packages. Coverage can be uploaded separately for each component and Codacy will aggregate the results.

Originally published at aicodereview.cc

Build a SMART on FHIR App in 30 Minutes

SMART on FHIR is how apps talk to electronic health records. It’s OAuth 2.0 with a few healthcare-specific conventions: a discovery document that tells your app where to authenticate, scopes that map to FHIR resource types, and a launch context that tells you which patient you’re looking at.

This tutorial follows SMART App Launch STU2.1 — the current published standard. If you’ve built an OAuth login flow before, you already understand 80% of it. The other 20% is what this covers.

We’re going to build a standalone SMART app that authenticates against a FHIR server, fetches a patient’s vital signs, and renders interactive charts — heart rate, blood pressure, SpO2, temperature, weight, and BMI. The whole thing fits in a single index.html file. No React, no bundler, no npm install. Just a text editor and a browser.

By the end you’ll have a working app and a mental model for how every SMART on FHIR app works under the hood — whether it’s a one-page demo or a production clinical decision support tool launching inside Epic.

Prerequisites

You need two things:

1. A mock.health account. Go to mock.health and sign up. It’s free. This gives you a FHIR R4 server with synthetic patients that actually have realistic clinical data — vital signs, conditions, medications, imaging studies.

2. A registered SMART app. In the mock.health dashboard, go to Sandbox → Register App and create a new app with these settings:

  • App Name: Vital Signs Chart (or whatever you want)
  • Redirect URI: http://localhost:8080/smart-on-fhir-vital-signs/index.html
  • Scopes: launch/patient, patient/Patient.rs, patient/Observation.rs, openid, fhirUser

Copy the Client ID — you’ll need it in a minute.

A note on scope syntax. SMART App Launch STU2 introduced a new scope format. The v1 format patient/Patient.read became patient/Patient.rs in v2, where the suffix letters map to individual operations: create, read, update, delete, search. So .rs means read + search, and v1’s .write maps to v2’s .cud. Servers advertise which format they support via permission-v1 and permission-v2 in their capabilities. Most production EHRs accept both formats today. This tutorial uses v2 (*.rs), but v1 (*.read) will also work with mock.health.

The redirect URI matters. It must exactly match the URL where your app is running. When the authorization server sends the user back to your app after login, it appends an authorization code to this URL. If it doesn’t match what you registered, the server rejects the request.

Step 1: Discover the SMART Endpoints

Every SMART-enabled FHIR server publishes a discovery document at /.well-known/smart-configuration. This tells your app where to send the user for authorization and where to exchange codes for tokens.

const baseUrl = 'https://api.mock.health';

const response = await fetch(`${baseUrl}/fhir/.well-known/smart-configuration`);
const config = await response.json();

// config.authorization_endpoint → where to redirect the user
// config.token_endpoint         → where to exchange the code for a token

The response looks like this:

{
  "issuer": "https://api.mock.health",
  "authorization_endpoint": "https://api.mock.health/smart/authorize",
  "token_endpoint": "https://api.mock.health/smart/token",
  "grant_types_supported": ["authorization_code"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["launch/patient", "patient/Patient.rs", "patient/Observation.rs", "openid", "fhirUser"],
  "capabilities": ["launch-standalone", "client-public", "context-standalone-patient", "permission-v1", "permission-v2", "sso-openid-connect"]
}

Why discovery instead of hardcoding? Because every FHIR server puts these endpoints at different paths. Epic uses /oauth2/authorize. Oracle Health (Cerner) uses /tenants/{tenant}/protocols/oauth2/profiles/smart-v1/personas/patient/authorize. The discovery document means your app works against any conformant server without code changes.

Step 2: Generate a PKCE Challenge

PKCE (Proof Key for Code Exchange, pronounced “pixie”) prevents authorization code interception attacks. Your app generates a random secret (the verifier), hashes it (the challenge), and sends only the hash to the authorization server. When you exchange the code for a token later, you prove you’re the same app by sending the original verifier.

function generateCodeVerifier(len = 64) {
  const arr = new Uint8Array(len);
  crypto.getRandomValues(arr);
  return base64url(arr);
}

async function computeCodeChallenge(verifier) {
  const digest = await crypto.subtle.digest(
    'SHA-256',
    new TextEncoder().encode(verifier)
  );
  return base64url(new Uint8Array(digest));
}

function base64url(bytes) {
  let bin = '';
  for (const b of bytes) bin += String.fromCharCode(b);
  return btoa(bin).replace(/+/g, '-').replace(///g, '_').replace(/=+$/, '');
}

Store the verifier in sessionStorage — you’ll need it after the redirect.

const verifier = generateCodeVerifier();
const challenge = await computeCodeChallenge(verifier);
sessionStorage.setItem('pkce_verifier', verifier);

Step 3: Redirect to Authorize

Build the authorization URL and redirect the user’s browser:

const state = crypto.randomUUID();
sessionStorage.setItem('auth_state', state);

const params = new URLSearchParams({
  response_type: 'code',
  client_id: clientId,
  redirect_uri: location.origin + location.pathname,
  scope: 'launch/patient patient/Patient.rs patient/Observation.rs openid fhirUser',
  state,
  aud: `${baseUrl}/fhir`,
  code_challenge: challenge,
  code_challenge_method: 'S256',
});

location.href = `${config.authorization_endpoint}?${params}`;

What each parameter does:

  • response_type: 'code' — We want an authorization code. Always code.
  • client_id — Identifies your app.
  • redirect_uri — Where to send the user back. Must exactly match what you registered.
  • scope — What data you’re requesting. launch/patient asks for a patient context. patient/Observation.rs asks to read and search Observations scoped to that patient.
  • state — A random string to prevent CSRF attacks.
  • aud — The FHIR endpoint this token should work with. Prevents token replay across servers.
  • code_challenge — The PKCE challenge.

After this redirect, the user sees a consent screen. When they approve, the server redirects back to your redirect_uri with ?code=...&state=... appended.

Step 4: Handle the Callback and Exchange the Code

When your page loads, check if there’s a code parameter in the URL:

const params = new URLSearchParams(location.search);
const code = params.get('code');
const state = params.get('state');

if (code) {
  if (state !== sessionStorage.getItem('auth_state')) {
    throw new Error('State mismatch — possible CSRF');
  }

  history.replaceState(null, '', location.pathname);

  const verifier = sessionStorage.getItem('pkce_verifier');

  const body = new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    redirect_uri: location.origin + location.pathname,
    client_id: clientId,
  });
  if (verifier) body.set('code_verifier', verifier);

  const response = await fetch(tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  });

  const tokens = await response.json();
}

The token response includes everything you need:

{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGci...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "launch/patient patient/Patient.rs patient/Observation.rs openid fhirUser",
  "patient": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "id_token": "eyJhbGciOiJSUzI1NiJ9..."
}

Two SMART-specific fields: patient is the launch context — the ID of the patient the user selected. id_token contains the authenticated user’s identity via the fhirUser claim.

Step 5: Fetch Vital Signs

Now you have a bearer token and a patient ID:

const headers = {
  Authorization: `Bearer ${tokens.access_token}`,
  Accept: 'application/fhir+json',
};

const patient = await fetch(`${baseUrl}/fhir/Patient/${tokens.patient}`, { headers })
  .then(r => r.json());

const observations = await fetch(
  `${baseUrl}/fhir/Observation?patient=${tokens.patient}&category=vital-signs&_sort=-date&_count=200`,
  { headers }
).then(r => r.json());

Each Observation has a LOINC code that identifies what it measures:

Vital Sign LOINC Code Value Location
Heart Rate 8867-4 valueQuantity.value
Blood Pressure 85354-9 component[].valueQuantity.value
Temperature 8310-5 valueQuantity.value
SpO2 2708-6 valueQuantity.value
Weight 29463-7 valueQuantity.value
BMI 39156-5 valueQuantity.value

Blood pressure is the odd one out. It’s a panel — the Observation itself has LOINC code 85354-9, but the actual systolic and diastolic values live in component entries with their own LOINC codes (8480-6 for systolic, 8462-4 for diastolic). Every FHIR developer hits this for the first time and wonders why it’s nested. Welcome to the club.

Step 6: Render the Charts

We use Chart.js — one CDN script tag, no build step:

<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>

Group observations by type, sort by date, render a line chart for each. For blood pressure, render two datasets on the same chart — systolic and diastolic.

The Complete App

The full working app is ~475 lines in a single index.html. Clone it and run it:

git clone https://github.com/mock-health/samples.git
cd samples
python3 -m http.server 8080

# Open http://localhost:8080/smart-on-fhir-vital-signs/index.html

Enter your Client ID, click Connect, approve access, and you’ll see your patient’s vital signs charted out.

The full source is on GitHub.

What Changes in Production EHRs

This tutorial uses mock.health, which implements the SMART spec faithfully. Production EHRs are different:

App registration takes weeks, not seconds. Epic’s App Orchard requires a formal review process. Budget 2–8 weeks for approval.

Scopes vary. Some EHRs still only accept v1 syntax (patient/Observation.read). Check the server’s .well-known/smart-configuration capabilities.

Discovery endpoints aren’t always where you expect. Some older servers don’t support .well-known/smart-configuration. Fall back to the CapabilityStatement (GET /metadata) and extract OAuth URLs from rest[0].security.extension.

Tokens expire and refresh tokens matter. In production, request offline_access scope and implement refresh token rotation.

None of this changes the fundamental flow. Discovery → PKCE → authorize → callback → token → API calls. Same everywhere. The operational details differ.

Sign up at mock.health — free tier, no sales call. The hardest part of building a SMART on FHIR app is getting access to a server that has realistic data and implements the spec correctly. Now you have one.

The Edge Isn’t a Place — It’s an Operating Reality

The mental model you start with determines every architecture decision that follows. Most teams are starting with the wrong one.

Edge computing used to mean “some devices send telemetry to the cloud.”

That era is over.

This is a re-post of Bruno Baloi’s blog Part 1: The Edge Isn’t a Place - It’s an Operating Reality on Synadia.com.

Today’s edge is a full operational domain where the physical world meets software systems: machines, vehicles, gateways, sensors, factories, field deployments. And once you move compute and messaging into that world, the rules change fast. Connections drop. Environments get hostile. Data gets generated faster than it can be forwarded. And the assumptions baked into a decade of cloud-native architecture patterns start failing in ways that are hard to diagnose because they fail quietly.

The first and most important shift is not a technical one, it’s a conceptual one.

The edge is not a far-away part of your system. It’s a different operating dimension entirely.

Why the geography framing gets you into trouble

When engineers hear “edge computing,” the mental image is usually spatial: devices on the left, cloud on the right, data flowing between them. The edge is just the far end of the pipeline.

That framing seems harmless until you start making architecture decisions based on it. If edge is just far-away infrastructure, you design for distance — low latency, efficient serialization, maybe some compression. You optimize the happy path.

What you don’t design for is disconnection as a first-class operating condition. Or physical exposure as a security assumption. Or the possibility that the “far end” of your system is running on hardware installed by a third-party contractor in an industrial cabinet that nobody has touched in eighteen months.

Those aren’t edge cases at the edge. They’re normal operating conditions. And the gap between “optimized for distance” and “designed for that reality” is where most edge architecture problems live.

The four constraints that don’t go away

If you’re building edge-to-core systems, you’re going to run into the same four problems regardless of industry, scale, or stack. Synadia’s Living on the Edge white paper names them clearly, and they’re worth exploring:

Connectivity. Edge links are intermittent by nature — not by failure. Devices roam in and out of coverage. Field gateways lose upstream access during maintenance windows. Vehicles move through dead zones. The question isn’t whether your edge nodes will disconnect; it’s whether your architecture treats that as an exception to handle or a condition to design for.

Security. Edge devices are physically exposed in ways that data center hardware never is. They’re accessible to anyone with physical proximity: maintenance crews, contractors, hostile actors with a USB drive and ten minutes to spare. Credentials get copied. Firmware gets tampered with. Unlike a compromised cloud instance that stays logically contained, a compromised edge device has a direct path toward your core systems if you haven’t designed the boundary carefully.

Distribution. Edge environments generate data at rates and granularities that cores can’t absorb naively. A manufacturing floor streaming sensor data from hundreds of machines isn’t a throughput problem — it’s a routing and filtering problem. The right data needs to reach the right destination at the right rate, which means the system has to be opinionated about what crosses the boundary and at what volume.

Observability. You need a real-time view across devices and the infrastructure connecting them to core — not just health checks, but end-to-end event traceability. Without it, you’re making operational decisions based on incomplete signals, and at the edge, incomplete signals tend to mean delayed incident detection and incorrect root cause analysis.

None of these four constraints are solvable by optimizing the happy path. They require deliberate design choices that change the shape of the architecture.

The reframe that unlocks the right patterns

Once you stop thinking of edge as geography and start thinking of it as a separate operational realm, the right patterns become obvious — or at least, the wrong patterns become obviously wrong.

The core insight, articulated in the Living on the Edge architecture guide, is this: treat edge and core as distinct realms connected by deliberately controlled paths. Not a single distributed system. Not an extended network. Two separate operating environments with an explicit, managed boundary between them.

That separation shows up in three practical ways:

Asynchronous communication instead of synchronous request-reply. HTTP works when the upstream is available and fast. At the edge, you can’t assume either. Asynchronous, event-driven communication means edge systems continue operating regardless of upstream availability — they produce events locally and let the transport layer handle delivery when conditions allow.

Store-and-forward instead of retry logic. When a link drops, edge systems shouldn’t be hammering a dead connection. They should be writing to a local durable store and resuming forwarding when connectivity restores — at a controlled rate that doesn’t overwhelm core consumers. (The second post in this series covers why this distinction matters more than most architects realize.)

Security realm constraints instead of shared credentials and open subjects. The boundary between edge and core should be explicit about what’s permitted to cross it. Not “encrypt everything and trust the network” — that’s perimeter thinking applied to a perimeter-free environment. The constraint lives in the topology, not just the transport.

This is where eventing becomes the backbone

If those three patterns share a common thread, it’s that they all require an eventing layer — not just messaging, but a platform that can handle asynchronous delivery, local durability, stream replication, and subject-level routing as composable primitives rather than as separate infrastructure.

Edge-to-core isn’t a “connect things to the cloud” problem. It’s a problem of how signals, commands, and durable streams move safely across unreliable, adversarial terrain — in both directions, at scale, with full traceability.

NATS and the Synadia Platform are purpose-built for exactly this architecture: leaf node topologies that treat edge clusters as first-class entities, JetStream for local durability and controlled forwarding, decentralized security for tightly scoped credentials, and end-to-end observability across the full mesh.

Questions worth asking before your next architecture review

Do you have a written definition of what “the edge” means in your system? If the answer is “the devices on the other side of the network,” the framing is still geographic. Try: “a separate operational realm with different connectivity, security, and observability characteristics than our core.”

Does your architecture document cover the disconnected case explicitly? If the connectivity section assumes the link is up, it’s documenting the happy path. The disconnected case is where edge architectures succeed or fail.

Are your edge and core security models the same? If your edge nodes use the same credentials, the same access scope, and the same network trust assumptions as your core services — you haven’t built a boundary. You’ve built a flat network with devices in inconvenient locations.

Do you know what your edge nodes are doing right now? Not in aggregate. Individually. If the answer is “we’d have to look at logs,” your observability model was designed for a data center, not an operational edge.

Getting these right doesn’t require exotic technology. It requires accepting that the edge is a different operational dimension — and designing for it from the start, not retrofitting resilience onto a system that assumed it wouldn’t need any.

This post is the first in a series exploring architecture patterns for resilient edge-to-core systems, based on Synadia’s white paper Living on the Edge: Eventing for a New Dimension.

Next up: why “just retry” is the wrong mental model for intermittent connectivity — and what happens when you find out the hard way.

Tags: Edge Computing · Distributed Systems · IoT · Software Architecture · Streaming · Microservices

How I Got Multiple AI Coding Agents to Stop Losing Track of Their Work

Task detail panel showing subtasks, artifacts, and metadata

I recently wrote about agent-comm — a communication layer that lets multiple AI coding agents talk to each other. But talking is only half the problem. The other half? Knowing what work needs to be done, who’s doing it, and what stage it’s in.

The problem

When you run multiple AI agents in parallel — say three Claude Code sessions working on different parts of a feature — things fall apart fast:

  • No shared backlog. Each agent only knows what you told it in its prompt. There’s no central place to see all pending work.
  • No pipeline visibility. Is the spec done? Has anyone started implementing? Did tests pass? Nobody knows.
  • No dependency tracking. Agent B can’t start until Agent A finishes, but there’s nothing enforcing that.
  • No artifacts. Specs, plans, test results — they live in chat context and vanish when the session ends.

I needed something that gives AI agents the same project management primitives that human teams take for granted — but exposed as MCP tools they can call directly.

The fix: a pipeline for AI agents

agent-tasks is an open-source pipeline-driven task management server that AI coding agents can use via MCP. Think of it as a lightweight Jira — but designed for machines, not humans.

Every task flows through configurable pipeline stages:

backlog → spec → plan → implement → test → review → done

Agents claim tasks, advance them through stages, attach artifacts at each step, and block on dependencies — all through 31 MCP tools.

What agents can do with it

1. Pipeline-driven workflow

Tasks aren’t just “open” or “closed.” They move through stages, and each stage produces artifacts:

  • Spec stage: Agent writes a specification and attaches it
  • Plan stage: Agent breaks the task into subtasks
  • Implement stage: Agent writes code, attaches a summary
  • Test stage: Agent runs tests, attaches results
  • Review stage: Another agent reviews and approves

2. Dependencies and blocking

task_add_dependency(task_id=5, depends_on=3, type="blocks")

Agent 5 literally cannot advance until task 3 is done. The system detects cycles too — no deadlocks.

3. Multi-agent collaboration

Agents can be assigned roles on tasks:

  • Collaborator: actively working on it
  • Reviewer: needs to approve before advancement
  • Watcher: gets notified of changes

4. Approval gates

task_request_approval(task_id=5, stage="review")
# Another agent:
task_approve(task_id=5)

This enforces a maker-checker pattern. No task moves to “done” without explicit sign-off.

5. Artifacts with versioning

Every spec, plan, test result, and review note is stored as a versioned artifact attached to the task. Previous versions are chained so you can see how decisions evolved.

6. Full-text search

Need to find that task about the auth middleware rewrite? task_search(query="auth middleware") uses SQLite FTS5 to search across all task titles and descriptions.

Setup

Install it:

npm install -g agent-tasks

Add to your MCP config (Claude Code settings.json, Cursor, etc.):

{
  "mcpServers": {
    "agent-tasks": {
      "command": "npx",
      "args": ["-y", "agent-tasks"]
    }
  }
}

That’s it. The dashboard auto-starts at http://localhost:3422.

A real coordination pattern

Here’s how I use it with three agents working on a feature:

Agent 1 (planner) creates the task and writes a spec:

task_create(title="Add rate limiting to API", project="backend")
task_claim(task_id=42)
task_add_artifact(task_id=42, name="spec", content="...")
task_advance(task_id=42)  # spec → plan

Agent 2 (implementer) picks up the next unblocked task:

task_next(project="backend")  # returns task 42 at plan stage
task_claim(task_id=42)
task_expand(task_id=42, subtasks=["Add middleware", "Add Redis store", "Add config"])
# ... implements each subtask ...
task_advance(task_id=42)  # implement → test

Agent 3 (reviewer) reviews and approves:

task_claim(task_id=42)
task_add_artifact(task_id=42, name="review-notes", content="LGTM, clean implementation")
task_approve(task_id=42)
task_advance(task_id=42)  # review → done

The whole flow is visible in the dashboard — a kanban board that updates in real-time via WebSocket.

The dashboard

The built-in dashboard gives you a kanban view of all tasks across pipeline stages. You can:

  • Drag tasks between columns
  • Filter by project, assignee, or priority
  • Expand task details with artifacts and comments
  • View artifact diffs between versions
  • Track subtask progress
  • Toggle dark/light theme

No frameworks — it’s vanilla HTML/CSS/JS with morphdom for efficient DOM updates.

Task detail panel showing subtasks, artifacts, and metadata

Dashboard in dark mode

Technical details

  • TypeScript + Node.js, zero-framework architecture
  • SQLite with WAL mode for concurrent reads
  • 3 transport layers: MCP (stdio), REST API (18 endpoints), WebSocket
  • 31 MCP tools covering tasks, subtasks, dependencies, artifacts, comments, approvals, and collaboration
  • 337+ tests with vitest
  • MIT licensed

It pairs naturally with agent-comm — when a task changes, agents get notified through the communication bridge.

What’s next

I’m actively using this to coordinate up to 5 Claude Code agents working simultaneously. If you’re running multi-agent setups and losing track of what’s happening, give it a try.

GitHub: github.com/keshrath/agent-tasks

Feedback welcome — open an issue or drop a comment below.