Every successful application faces the same pressure: someone needs a capability that is genuinely valuable and genuinely not core. A visual architecture explorer. A storefront. A pipeline editor. Each is real work that some users need and most do not.
Put them all in core and you get a codebase where every deployment carries every feature, startup loads code nobody uses, and the navigation menu becomes an inventory rather than a tool. Refuse them all and you get a rigid framework people fork.
Tiknix uses two mechanisms in combination: feature flags and sidecars.
Feature flags with a privilege floor
lib/Feature.php is a per-member flag system with one design decision that makes it
more than a boolean store: every flag declares a minimum privilege level.
public const CATALOG = [
'explorer' => [
'label' => 'Architecture Explorer',
'blurb' => 'Visual data-model + call-graph explorer for your instances.',
'min_level' => 100, // MEMBER and above
],
'shop' => [
'label' => 'Store',
'blurb' => 'Per-instance storefront + admin, checkout via that instance\'s own Stripe.',
'min_level' => 100,
],
'pipelines' => [
'label' => 'Pipeline Editor',
'blurb' => 'Build, edit, run and schedule deterministic pipelines in your instances.',
'min_level' => 100,
],
];
Flags are stored as feature.<key> rows in the existing member-scoped settings table — no new subsystem, no new table, no new admin screen concept. And the read path re-checks eligibility every time:
isEnabled() re-checks eligibility on every read, so a demotion silently revokes the
flag without any cleanup pass.
That is the property worth copying. In most flag systems, granting a capability writes a row, and the row outlives the reason it was granted. Someone is demoted, and their flags quietly remain — a privilege-escalation path assembled entirely out of correct-looking individual operations.
Here, eligibility is evaluated at read time from the member's current level. A demotion revokes access immediately, everywhere, with no cleanup job to write, schedule, or forget. The flag is a grant plus a live eligibility check, and both must hold.
Sidecars: separate apps, shared identity
A sidecar is a full application in its own repository — the architecture explorer, the shop, the pipeline editor — that a member reaches from your navigation and logs into with their existing account.
Registering one is configuration, not code:
[sidecar.explorer]
url = https://explorer.tiknix.com
sso_secret = <shared with the plugin's [sidecar] sso_secret>
feature = explorer ; the Feature flag gating who may launch it
label = Architecture Explorer
icon = bi-diagram-3
Core reads these sections, shows a launch link to members whose feature flag is on, and mints a
signed handoff token. Adding a plugin is an ini section. Removing one is deleting the
section.
The SSO handoff, and why it is careful
This is where a naive implementation gets dangerous, so it is worth reading what
lib/Sidecar/Sso.php actually does when a sidecar consumes a handoff token:
- Verify the signature, with an audience claim naming the specific plugin — a token for the shop cannot be replayed at the explorer.
- Burn the nonce, single-use. A replayed token is rejected outright.
- Re-check against core's database that the member is still active and still holds the feature grant — so a revoke on core propagates immediately rather than at token expiry.
- Regenerate the session id — standard session-fixation defense at a privilege transition.
- Store the minimum: just
{member_id, level, email}, namespaced under the plugin name.
Step 3 is the one that separates this from an ordinary SSO handoff. Most token-based SSO trusts the token's claims for its lifetime, which means revocation does not take effect until it expires. Re-validating against core on consumption costs one query and closes that window.
What this architecture buys
| Property | Consequence |
|---|---|
| Separate repository | Heavy features do not bloat core's clone, dependencies, or startup |
| Separate deploy | The explorer can ship on its own schedule; a bug there does not take down core |
| Shared identity | Users get one account and one login, not a second product to sign up for |
| Flag-gated | Only members who need it see it — the menu stays a tool, not an inventory |
| Config-registered | Adding or removing a plugin touches no code |
The comment in Feature.php records a real migration along these lines: an in-core
ecommerce flag was removed and the store became the shop.tiknix sidecar. Core got
smaller; the capability stayed available. That is the intended direction of travel — features
graduate out of core rather than accumulating in it.
When to reach for which
- Feature flag alone — a capability that is small, lives in core, and only some members should see. A few files, one nav item.
- Sidecar plus flag — a capability with its own dependencies, its own release cadence, its own data model, or enough weight that core should not carry it.
The honest test is dependency weight and deploy independence. If it needs libraries core does not and would slow every deploy, it is a sidecar.
- Sidecars are separate operational responsibilities. Another deploy, another certificate, another log to watch, another thing that can be down while core is fine. Do not adopt one to avoid writing a controller.
- The SSO secret is shared state. Core and the plugin must hold the same secret. Rotating it requires coordinating both sides, and getting it wrong logs everybody out of the sidecar.
- The flag catalog is a constant. Flags live in a class constant, so adding one is a code change and a deploy — not a runtime toggle. That is a reasonable trade for auditability, but it is not a feature-management platform with percentage rollouts.
- Flags are per-member, not per-cohort. There is no "10% of users" targeting. Granting to fifty people is fifty grants.
- Cross-app debugging is harder. When something breaks at the handoff, you are reading two applications' logs and correlating by hand.
- A launch link is not authorization. The sidecar re-checks the grant on consumption, which is what makes hiding the nav item a UI nicety rather than the security boundary. Any sidecar you write must keep that check — inherit
Ssorather than reimplementing it.