MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

Telegram and the Architectural Shift Toward Access-Layer Authentication

2026-03-04 03:21:40

Telegram has added OpenID Connect support with Authorization Code Flow and PKCE to its authentication system.

This is not simply a login update. Architecturally, it reflects a broader shift: authentication is increasingly designed as a structured access layer within digital systems.

Protocol-Driven Access

With OIDC in place, access is formalized through:

  • Authorization Code Flow
  • PKCE
  • ID tokens
  • Signature verification
  • Issuer and audience validation
  • Strict redirect_uri control

The login process becomes a standardized protocol for negotiating access between client, browser, and server.

Login evolves into a formal access issuance mechanism.

From Identity-Centric to Access-Centric Design

Traditional authentication systems centered around identity storage:

  • User accounts
  • Profile attributes
  • Credential verification
  • Password recovery

Modern architectures increasingly center around access control:

  • When is access granted?
  • Under which scope?
  • For how long?
  • Under what validation guarantees?

Identity remains part of the system.

Access becomes the architectural focus.

Access as a Dedicated Layer

When authentication is implemented through OIDC + PKCE, attention shifts toward:

  • Session issuance
  • Token lifecycle
  • Scope definition
  • Cryptographic validation
  • Lifetime enforcement

This defines an access layer — a component responsible for governing how access is negotiated, issued, and validated.

Such a layer integrates cleanly with existing authentication stacks and access management systems.

Trusted Client Confirmation

Telegram’s flow includes confirmation inside the application itself.

Architecturally, this:

  • Binds the browser session to an authenticated client
  • Moves confirmation into a trusted environment
  • Reduces exposure to phishing-style credential capture

Session binding becomes part of the access architecture.

Scoped and Contextual Access

Use of scopes (e.g., phone sharing, communication permissions) structures access as a defined set of rights.

This model introduces:

  • Explicit permission negotiation
  • Context-bound access
  • Clearly defined capability boundaries

Authorization becomes a controlled issuance of rights with defined parameters.

Architectural Direction

Standardized, protocol-driven authentication models point toward a clear architectural direction:

  • Access mechanisms are formalized
  • Login flows are protocolized
  • Session issuance is cryptographically verifiable
  • Access control is treated as infrastructure

Authentication increasingly functions as a dedicated access layer within system design.

Telegram represents one example of this broader architectural evolution.

Access-layer design is becoming a norm rather than an exception in modern digital systems.

SkyDiscover: An Open Framework for LLM-Driven Algorithm Discovery

2026-03-04 03:20:51

SkyDiscover modular discovery loop animation
We are open-sourcing SkyDiscover, a modular framework for AI-driven algorithm discovery.

Most prior systems (e.g., AlphaEvolve) are closed-source. Existing open implementations are monolithic and hard to extend. SkyDiscover decomposes the discovery loop into four swappable components:

  1. Context Builder
  2. Solution Generator
  3. Evaluator
  4. Selector

On top of this framework, we implemented:

  • AdaEvolve — adaptive search
  • EvoX — self-modifying search

Results (200+ Benchmarks)

Across math, systems, programming, and multimodal tasks:

  • +34% median on 172 Frontier-CS problems (vs prior open methods)
  • Matched/exceeded AlphaEvolve on several math + systems tasks
  • 41% lower cross-cloud transfer cost
  • 29% lower KV-cache pressure

SkyDiscover provides a clean interface for building, comparing, and extending discovery algorithms.

I ran cursor-doctor on 50 real projects. Here's what broke.

2026-03-04 03:02:52

I wanted to know: how healthy are Cursor rules in the wild? Not in tutorials or curated examples, but in real GitHub repos where people are actually shipping code.

So I cloned 50 random public repos that use Cursor rules and ran npx cursor-doctor scan on every one of them.

The results: 60% of projects scored a C

A:  3 ███                          (6%)
B: 15 ███████████████              (30%)
C: 30 ██████████████████████████████ (60%)
D:  2 ██                           (4%)

Average health score: 67%. Only 3 out of 50 projects had healthy Cursor rules. The median project scored 69%, which is a C. That means most Cursor setups have enough issues to noticeably affect how well the AI follows instructions.

I dug deeper into 15 of those projects and categorized every issue cursor-doctor flagged. 998 total issues. Here are the five problems that kept showing up.

1. Rules that eat your context window (146 issues)

This was the single most common problem. Nearly 15% of all issues were rules that are too long.

Rules with 2,000+ characters each. Some over 5,000. Each one burns 500 to 1,250 tokens of your context window before Cursor even reads your code.

One project had 12 rules averaging 4,000 characters each. That is roughly 12,000 tokens of instructions alone, leaving less room for the code Cursor needs to work with.

The fix: Split long rules into focused ones. Remove the parts that repeat what Cursor already knows. Cut examples down to one or two instead of eight.

2. Rules that target nothing (36 issues)

Empty globs arrays. The rule exists, has a description, has instructions, but the globs field is []. Cursor doesn't know which files the rule applies to.

---
description: "React component standards"
globs: []
---

The fix: Add the right glob pattern (**/*.tsx) or set alwaysApply: true if the rule should apply everywhere.

3. Vague instructions Cursor ignores (27 issues)

"Try to keep functions small." "Consider using TypeScript." "Maybe add error handling."

Cursor doesn't try, consider, or maybe. It either follows the instruction or it doesn't. Weak language gives the model permission to ignore you.

Compare: "Try to use TypeScript" vs "All new files must use TypeScript. No .js files except config files in the project root."

4. Dead code burning tokens (32 issues)

Commented-out sections, TODO markers, notes-to-self. These all consume tokens and confuse the model about what is actually a rule and what is a draft.

<!-- TODO: add React rules later -->
<!-- OLD: use class components -->

If it is commented out, delete it. Rules are not source code. There is no reason to keep old versions around.

5. Missing or broken frontmatter (48 issues)

28 projects had rules with no description. 20 had rules with no YAML frontmatter at all. Without frontmatter, Cursor cannot properly categorize or prioritize the rule. Without a description, you have no idea what the rule does when you revisit your setup six months later.

A properly formatted rule file needs at minimum:

---
description: Enforces consistent error handling in API routes
globs: "src/api/**/*.ts"
---
All API route handlers must wrap their body in a try/catch block.
Return a 500 status with a generic error message. Log the full error to the server console.

What the healthy projects did differently

The 3 projects that scored an A had a few things in common:

  • Short, focused rules. One concern per file. Under 1,000 characters each.
  • Correct frontmatter. Every rule had a description, appropriate globs, and explicit alwaysApply settings.
  • Concrete instructions. No "try to" or "consider." Direct statements with examples.
  • No dead weight. No commented code, no TODOs, no redundant instructions.

Check your own Cursor rules

npx cursor-doctor scan

Takes about 2 seconds. Gives you a health grade from A through F and tells you exactly what to fix. The scan and lint commands are free, no install needed.

For a detailed breakdown of every issue, file by file:

npx cursor-doctor lint

cursor-doctor on GitHub

Also available as a VS Code and Cursor extension with inline diagnostics.

Stop Writing .subscribe() in Angular Components — Use toSignal Instead

2026-03-04 03:00:00

If you've been writing Angular for more than a week, you know this pattern by heart:

protected userData = signal<UserData | undefined>(undefined);

ngOnInit() {
  this.userService.getUserData()
    .pipe(
      tap(data => this.userData.set(data)),
      takeUntilDestroyed(this.destroyRef)
    )
    .subscribe();
}

Signal. Subscribe. tap. set. Cleanup. Every. Single. Time.

It works, but there's a lot of moving parts for something that boils down to "I want to show this data in my template." Turns out Angular has a one-liner for exactly this — toSignal.

The core idea

Observables and Signals live in different worlds. Observables are async — they emit values over time, can be cold or hot, and can stay silent for a while. Signals are synchronous — they always have a current value and work directly in templates without any pipe magic.

toSignal is the bridge. You hand it an Observable, it gives you back a Signal that:

  • subscribes automatically
  • updates on every emission
  • cleans itself up when the component is destroyed

Zero .subscribe() calls in your component.

Basic usage

import { toSignal } from '@angular/core/rxjs-interop';

@Component({ ... })
export class UserComponent {
  private readonly userService = inject(UserService);

  protected userData = toSignal(this.userService.getUserData());
}

That's it. No ngOnInit, no DestroyRef, no takeUntilDestroyed. The type is inferred automatically — because getUserData() returns Observable<UserData>, you get Signal<UserData | undefined> back.

In the template:

@if (userData(); as user) {
  <h2>{{ user.name }}</h2>
} @else {
  <p>Loading...</p>
}

No async pipe. Just call it like a function.

One rule to remember: toSignal must be called inside an injection context — a constructor, a field initializer, or a function called during construction. Same rules as inject().

Why | undefined?

There's a gap between when your component loads and when the Observable actually emits. Observables can be silent — Signals cannot. Before the first emission, the Signal needs something to hold. That something is undefined by default, hence Signal<T | undefined>.

You have two options to get rid of it.

Option 1: initialValue

Use this when you have a sensible empty state — an empty array, an empty string, a default object.

protected posts = toSignal(
  this.userService.getUserPosts(1),
  { initialValue: [] as Post[] }
);
// Type: Signal<Post[]> — no undefined

Option 2: requireSync

Use this when your Observable is guaranteed to emit synchronously — like a BehaviorSubject.

protected liveUser = toSignal(
  this.userService.getUserDataHot(),
  { requireSync: true }
);
// Type: Signal<UserData> — TypeScript knows it's never undefined

If you get this wrong and the Observable doesn't emit synchronously, Angular tells you immediately:

Error: NG0601: toSignal() was called with requireSync,
but the Observable did not emit synchronously.

No silent bugs. Fast feedback.

What about multiple signals from the same source?

This is where it gets slightly more interesting. If you need two signals derived from the same Observable, naively doing this creates two separate subscriptions:

protected userData = toSignal(this.userService.getUserData());
protected posts = toSignal(
  this.userService.getUserData().pipe(
    switchMap(data => this.userService.getUserPosts(data.id))
  )
);

The fix is shareReplay(1) — share the upstream, and replay the last value to any late subscribers:

private readonly userData$ = this.userService.getUserData().pipe(shareReplay(1));

protected userData = toSignal(this.userData$);
protected posts = toSignal(
  this.userData$.pipe(
    switchMap(data => this.userService.getUserPosts(data.id))
  ),
  { initialValue: [] as Post[] }
);

shareReplay(1) does two things: it multicasts (one HTTP request, multiple consumers) and it replays the last value to subscribers who come in slightly later — which matters because toSignal subscribes at slightly different times internally.

Auto-cleanup — same engine as takeUntilDestroyed

If you've seen the takeUntilDestroyed pattern, the cleanup story here is identical. toSignal internally grabs DestroyRef from the injection context and unsubscribes when the component is destroyed. You don't have to think about it.

That's also why the injection context requirement exists — without it, toSignal has no way to know when to clean up.

Quick reference

// Default — use when Observable is async and you have no meaningful fallback
toSignal(obs$)
// → Signal<T | undefined>

// initialValue — use when you have a sensible empty state
toSignal(obs$, { initialValue: [] as T[] })
// → Signal<T>

// requireSync — use with BehaviorSubject or any synchronous Observable
toSignal(obs$, { requireSync: true })
// → Signal<T>

Before / After

Before:

protected userData = signal<UserData | undefined>(undefined);
private readonly destroyRef = inject(DestroyRef);

ngOnInit() {
  this.userService.getUserData()
    .pipe(
      tap(data => this.userData.set(data)),
      switchMap(data => this.userService.getUserPosts(data.id)),
      takeUntilDestroyed(this.destroyRef)
    )
    .subscribe(posts => this.posts.set(posts));
}

After:

private readonly userData$ = this.userService.getUserData().pipe(shareReplay(1));

protected userData = toSignal(this.userData$);
protected posts = toSignal(
  this.userData$.pipe(switchMap(d => this.userService.getUserPosts(d.id))),
  { initialValue: [] as Post[] }
);

The component no longer knows that Observables even exist. It just has Signals.

This is part 2 of a series on modern Angular patterns. Part 1 covered takeUntilDestroyed and Hot vs Cold Observables. Next up: toObservable — going the other direction, from Signal back to Observable.

Mocking HTTP Services in PHP with Phiremock

2026-03-04 02:57:49

This article is part of a series on mock servers for backend developers. Part 1 covers language-agnostic tools (Postman, WireMock, and Pact Stub Server). This article focuses on Phiremock, the first of three PHP-specific tools covered in the series.

When your PHP application integrates with third-party APIs — payment gateways, logistics providers, marketplaces — testing those integrations reliably is a real challenge. Hitting real sandboxes is slow, fragile, and sometimes billable. Phiremock solves this by running a real HTTP server that your application calls instead, returning whatever response you define.

What is Phiremock?

Phiremock is a standalone HTTP mock server written in PHP, heavily inspired by WireMock. You run it as a separate process (or Docker container) and configure it via JSON expectation files or programmatically through its PHP client.

Current version: v1.5.1 (released December 2024)
PHP compatibility: ^7.2 | ^8.0
GitHub: https://github.com/mcustiel/phiremock

The key advantage over in-process mocking libraries is that Phiremock runs as a real HTTP server. Your application doesn't know it's talking to a mock; it makes real HTTP calls, making the tests more realistic.

Installation

Since version 2, Phiremock is split into two packages: the server and the client. Install both as dev dependencies:

composer require --dev mcustiel/phiremock-server mcustiel/phiremock-client

If you also need Guzzle as the HTTP client (default):

composer require --dev mcustiel/phiremock-server mcustiel/phiremock-client guzzlehttp/guzzle:^6.0

Starting the Server

Start Phiremock from the command line after installing:

vendor/bin/phiremock

By default, it listens on 0.0.0.0:8086. To customise:

vendor/bin/phiremock --port 8089 --ip 127.0.0.1 --debug

Configuration File

For a consistent setup, create a .phiremock file at your project root:

<?php 
  return [
      'port'             => 8089,
      'ip'               => '0.0.0.0',
      'expectations-dir' => './tests/expectations',
      'debug'            => false,
  ];

Phiremock searches for this file automatically at startup. Command-line arguments take priority over the config file.

Docker Compose

For a shared dev/CI environment, run Phiremock alongside your application:

services:
  app:
    build: .
    environment:
      PAYMENT_API_URL: http://phiremock:8086
    depends_on:
      - phiremock

  phiremock:
    image: mcustiel/phiremock
    ports:
      - "8086:8086"
    volumes:
      - ./tests/expectations:/var/www/phiremock/expectations

Once running, update your application config to point to http://localhost:8086 (or the Docker service name) instead of the real service URL.

Defining Expectations

An expectation tells Phiremock: "When you receive this request, return this response". You can define expectations in two ways: as JSON files loaded on startup, or programmatically via the PHP client at runtime.

JSON Expectations (Declarative)

Place JSON files in your expectations-dir directory. They are loaded automatically when Phiremock starts.
Match a GET request and return a product:

{
  "version": "2",
  "on": {
    "method": { "isSameString": "GET" },
    "url": { "isEqualTo": "/api/products/1" }
  },
  "then": {
    "response": {
      "statusCode": 200,
      "body": "{\"id\": 1, \"name\": \"Gift Box\", \"price\": 49.99}",
      "headers": { "Content-Type": "application/json" }
    }
  }
}

Match a POST with a URL regex and simulate a 100ms delay:

{
  "version": "2",
  "on": {
    "method": { "isSameString": "POST" },
    "url": { "matches": "~/api/orders/?~" }
  },
  "then": {
    "delayMillis": 100,
    "response": {
      "statusCode": 201,
      "body": "{\"orderId\": \"abc-123\", \"status\": \"processing\"}",
      "headers": { "Content-Type": "application/json" }
    }
  }
}

Simulate a 503 service unavailable

{
  "version": "2",
  "on": {
    "method": { "isSameString": "GET" },
    "url": { "isEqualTo": "/api/inventory/check" }
  },
  "then": {
    "response": {
      "statusCode": 503,
      "body": "{\"error\": \"Service temporarily unavailable\"}",
      "headers": { "Content-Type": "application/json" }
    }
  }
}

Programmatic Expectations (PHP Client)

For dynamic test scenarios, define expectations in code using the Phiremock client. This is particularly useful in PHPUnit tests where each test needs a different response.

Setting up the client:

<?php

use Mcustiel\Phiremock\Client\Connection\Host;
use Mcustiel\Phiremock\Client\Connection\Port;
use Mcustiel\Phiremock\Client\Factory;
use Mcustiel\Phiremock\Client\Phiremock;
use Mcustiel\Phiremock\Client\Utils\A;
use Mcustiel\Phiremock\Client\Utils\Is;
use Mcustiel\Phiremock\Client\Utils\Respond;

$client = Factory::createDefault()->createPhiremockClient(
    new Host('localhost'),
    new Port(8086)
);

Creating the expectation

$client->createExpectation(
    Phiremock::on(
        A::getRequest()->andUrl(Is::equalTo('/api/products/1'))
    )->then(
        Respond::withStatusCode(200)
            ->andBody('{"id": 1, "name": "Gift Box", "price": 49.99}')
            ->andHeader('Content-Type', 'application/json')
    )
);

Matching by body content:

$client->createExpectation(
    Phiremock::on(
        A::postRequest()
            ->andUrl(Is::equalTo('/api/orders'))
            ->andBody(Is::containing('"productId":1'))
    )->then(
        Respond::withStatusCode(201)
            ->andBody('{"orderId": "abc-123"}')
            ->andHeader('Content-Type', 'application/json')
    )
);

Matching by header:

$client->createExpectation(
    Phiremock::on(
        A::getRequest()
            ->andUrl(Is::equalTo('/api/products'))
            ->andHeader('Authorization', Is::matching('/^Bearer .+/'))
    )->then(
        Respond::withStatusCode(200)
            ->andBody('[{"id": 1, "name": "Gift Box"}]')
            ->andHeader('Content-Type', 'application/json')
    )
);

Using with PHPUnit

Here is a complete PHPUnit test class that resets Phiremock state between tests and uses programmatic expectations:

Defining a config to run before the tests:

<?php

namespace App\Tests;

use Mcustiel\Phiremock\Client\Connection\Host;
use Mcustiel\Phiremock\Client\Connection\Port;
use Mcustiel\Phiremock\Client\Factory;
use Mcustiel\Phiremock\Client\Phiremock;
use Mcustiel\Phiremock\Client\Utils\A;
use Mcustiel\Phiremock\Client\Utils\Is;
use Mcustiel\Phiremock\Client\Utils\Respond;
use PHPUnit\Framework\TestCase;

class ProductServiceTest extends TestCase
{
    private static Phiremock $phiremock;
    private ProductService $productService;

    public static function setUpBeforeClass(): void
    {
        self::$phiremock = Factory::createDefault()->createPhiremockClient(
            new Host('localhost'),
            new Port(8086)
        );
    }

Defining the set up for the tests:

    protected function setUp(): void
    {
        // Clear all expectations and request history before each test
        self::$phiremock->clearExpectations();
        self::$phiremock->resetRequestsLog();

        // Point your service at the mock server
        $this->productService = new ProductService('http://localhost:8086');
    }

Writing the functions to test:

[Positive Scenario]

    public function testFetchesProductSuccessfully(): void
    {
        self::$phiremock->createExpectation(
            Phiremock::on(
                A::getRequest()->andUrl(Is::equalTo('/api/products/1'))
            )->then(
                Respond::withStatusCode(200)
                    ->andBody('{"id": 1, "name": "Gift Box", "price": 49.99}')
                    ->andHeader('Content-Type', 'application/json')
            )
        );

        $product = $this->productService->find(1);

        $this->assertEquals('Gift Box', $product['name']);
        $this->assertEquals(49.99, $product['price']);
    }

[Negative Scenario]

    public function testHandles404Gracefully(): void
    {
        self::$phiremock->createExpectation(
            Phiremock::on(
                A::getRequest()->andUrl(Is::equalTo('/api/products/999'))
            )->then(
                Respond::withStatusCode(404)
                    ->andBody('{"error": "Not found"}')
                    ->andHeader('Content-Type', 'application/json')
            )
        );

        $this->expectException(ProductNotFoundException::class);
        $this->productService->find(999);
    }

    public function testHandlesServiceTimeout(): void
    {
        self::$phiremock->createExpectation(
            Phiremock::on(
                A::getRequest()->andUrl(Is::equalTo('/api/products/1'))
            )->then(
                Respond::withStatusCode(200)
                    ->andBody('{"id": 1, "name": "Gift Box"}')
                    ->andDelayInMillis(3000) // simulate a slow response
            )
        );

        $this->expectException(ServiceTimeoutException::class);
        $this->productService->find(1);
    }

Verifying Calls

One of Phiremock's most useful features for strict test assertions is verifying that your application actually made the expected calls — not just that it returned the right result:

public function testCreatesOrderAndCallsPaymentGateway(): void
{
    self::$phiremock->createExpectation(
        Phiremock::on(
            A::postRequest()->andUrl(Is::equalTo('/api/payments/charge'))
        )->then(
            Respond::withStatusCode(200)
                ->andBody('{"chargeId": "ch_abc123", "status": "succeeded"}')
        )
    );

    $this->orderService->create(['productId' => 1, 'quantity' => 2]);

    // Assert the payment endpoint was called exactly once
    $executions = self::$phiremock->countExecutions(
        A::postRequest()->andUrl(Is::equalTo('/api/payments/charge'))
    );
    $this->assertEquals(1, $executions);
}

Stateful Scenarios

Phiremock supports scenarios (sequences of responses that transition state depending on how many times an endpoint has been called). This is useful for simulating async workflows, such as order processing or job polling.
Define two JSON expectations for the same endpoint with different scenario states:

First call — returns "processing":

{
  "version": "2",
  "scenarioName": "order-lifecycle",
  "on": {
    "scenarioStateIs": "Scenario.START",
    "method": { "isSameString": "GET" },
    "url": { "isEqualTo": "/api/orders/abc-123" }
  },
  "then": {
    "newScenarioState": "order-completed",
    "response": {
      "statusCode": 200,
      "body": "{\"orderId\": \"abc-123\", \"status\": \"processing\"}",
      "headers": { "Content-Type": "application/json" }
    }
  }
}

Second call — returns "completed":

{
  "version": "2",
  "scenarioName": "order-lifecycle",
  "on": {
    "scenarioStateIs": "order-completed",
    "method": { "isSameString": "GET" },
    "url": { "isEqualTo": "/api/orders/abc-123" }
  },
  "then": {
    "response": {
      "statusCode": 200,
      "body": "{\"orderId\": \"abc-123\", \"status\": \"completed\"}",
      "headers": { "Content-Type": "application/json" }
    }
  }
}

The first time your application calls GET /api/orders/abc-123, it receives a processing status. The second call returns completed — no code changes needed between calls.
You can reset all scenarios to their initial state between tests via the client:

self::$phiremock->resetScenarios();

Pros & Cons

Pros:

  • Supports PHP 8, actively maintained (v1.5.1, December 2024)
  • Runs as a real HTTP server — tests are realistic end-to-end
  • Rich matching API: method, URL, body, headers, regex, exact match
  • Network latency simulation with delayMillis
  • Stateful scenarios for async workflows
  • Verify call counts for strict assertion
  • Docker image available for shared environments
  • Works with any PHP HTTP client

Cons:

  • Requires a running server process — adds infrastructure overhead
  • No built-in GUI (unlike Postman or WireMock)
  • The client requires Guzzle v6 by default (an older version)
  • More setup than in-process libraries for simple unit tests

When to Use Phiremock

Phiremock is the right choice when you need a mock server that multiple test suites, developers, or services can share, especially in a Docker Compose environment. If you need stateful scenarios, call verification, or realistic end-to-end HTTP testing where your app makes actual network connections to a real server, Phiremock is the strongest PHP-native option available.
For simpler, inline mocks that live entirely inside a single PHPUnit test class, take a look at the next tools in this series.

This article is part of a series on mock servers. Read Part 1: Language-Agnostic Tools (Postman, WireMock, Pact Stub Server), or continue with the next PHP tool in the series.

If you have questions or want to share how your team uses Phiremock, drop a comment below!

— Bruno Souza | Senior Software Engineer | LinkedIn