A WordPress Integration Story

A WordPress Integration Story

It All Started with Metadata

Milk Admin natively relies on two separate databases — one for internal configuration, the other for managing external data. This architecture made it a natural candidate for integration with external systems, such as a WordPress database.

Once connected, I was able to start managing a WordPress site's tables directly from my panel. From there, an immediate need emerged: handling metadata relationships natively. I looked at how the most well-known and mature systems similar to mine handled this, and I found that there usually isn't a native system for it. So I wrote hasMeta, a method that handles native metadata relationships.

How it looks in a model definition

$rule->table('#__users')
    ->id()
        ->hasMeta('nickname', UserMetaModel::class, 'user_id')
        ->hasMeta('city', UserMetaModel::class, 'user_id')
    ->string('name', 100)->required();

Then there are a number of other small conventions I inherited from WordPress: the dynamic table prefix (#__) and a functions.php file for more advanced customizations.


A Small Personal Win

Every now and then I try to promote my work — I'm terrible at it, but I try. When I showed the metadata functionality to a client of mine, his reaction was enthusiastic. He asked me for a demo… that had nothing to do with metadata. He wanted to verify whether my admin panel could handle WordPress authentication.

A different request than expected, but an exciting one, so I got to work.


The WordPress Plugin

I developed a plugin that hooks into WordPress's authenticate filter, with priority 30, to intervene after the standard checks:

add_filter('authenticate', [$this, 'authenticate'], 30, 3);

The method receives three parameters: $user (the result of previous checks), $username, and $password. If conditions require it, the plugin makes a POST call to my admin panel's endpoint via wp_remote_post:

$args = [
    'headers' => ['Content-Type' => 'application/json'],
    'timeout' => (int)$options['request_timeout'],
    'body'    => wp_json_encode([
        'username' => $username,
        'password' => $password,
    ]),
];

$response = wp_remote_post($endpoint, $args);

The Authentication Endpoint

On the Milk Admin side, I created a dedicated module that exposes a public endpoint for credential verification:

#[ApiEndpoint('public-auth/check-credentials', 'POST')]

Reachable with a simple call:

POST /public_html/api.php?page=public-auth/check-credentials
Content-Type: application/json

{
  "username": "user@example.com",
  "password": "password"
}

For this demo the table only contains username and password, but nothing prevents extending the response with additional data such as roles, permissions, or profile information.


The Complete Flow

Here's what happens when a user tries to log in:

[Browser] → WordPress login
     │
     ▼
[wp-login.php] → authenticate filter (priority 30)
     │
     ├─ username/password empty?     → native WordPress flow
     ├─ user is administrator?       → native WordPress flow
     │
     ▼
[wp_remote_post] → POST to admin panel
     │
     ▼
[Admin Panel — CredentialsApi]
     ├─ Reads the table with access credentials
     ├─ Verifies the hash: HMAC-SHA384 → Base64 → bcrypt
     └─ Responds with {"success": true, "data": {"authenticated": true}}
     │
     ▼
[WordPress Plugin]
     ├─ Authentication failed?  → WP_Error, login denied
     ├─ Searches for local user (by meta, email, or login)
     │    ├─ Found    → updates meta and returns WP_User
     │    └─ Not found → creates a new local user → WP_User
     │
     ▼
[WordPress] → session opened, redirect to homepage

Key design choices

The plugin is designed not to interfere with administrators and not to block the native flow when username or password are empty: in those cases, WordPress handles everything as usual.


Final Thoughts

What started as an experiment with metadata turned into a concrete demonstration of interoperability. My admin panel doesn't replace WordPress — it works alongside it: it makes it easy to create a management panel that extends WordPress data, but I discovered it can also integrate well via API.

You'll find attached the Milk Admin module and the WordPress plugin in case anyone wants to try them out.


MilkAdmin module for WordPress integration
WordPress plugin.