Selected practice

Projects

Things I designed, built, maintained, or explored.

Current Focus

2026 · active

Aegis

Centralized, explainable authorization with fine-grained access control, multi-tenancy, and auditability.

C# · .NET · ASP.NET Core · Authorization · RBAC · ReBAC

The problem

Authorization tends to begin as a handful of role checks and slowly spreads across controllers, services, and database queries. Aegis explores a different boundary: permissions, relationships, decisions, and explanations belong to a central authorization platform.

That boundary becomes especially important in multi-tenant software. A user may administer one tenant, observe another, and have no relationship with a third. A single IsAdmin flag cannot express that context safely. The system needs to answer not only who is this user?, but what relationship does this subject have with this resource inside this tenant?

Context and constraints

Aegis is designed as an engineering study of production authorization rather than a collection of controller attributes. Its constraints shape the architecture:

  • every decision belongs to a tenant boundary;
  • policy evaluation must be deterministic and testable;
  • callers need a small, stable authorization contract;
  • administrators need an audit trail without exposing sensitive internals;
  • denial should be explainable without turning the explanation into an information leak;
  • role-based and relationship-based models should be able to coexist.

These constraints deliberately move policy orchestration away from HTTP endpoints and domain services. Application code asks a question; the authorization system owns how that question is evaluated.

Architectural shape

flowchart LR Client[Product application] --> API[Authorization API] API --> Boundary[Tenant and store boundary] Boundary --> Resolve[Resolve subject, action, and resource] Resolve --> Evaluate[Policy and relationship evaluation] Evaluate --> Decision[Explainable decision] Decision --> Client Decision --> Audit[(Audit record)] Model[(Authorization models)] --> Evaluate Tuples[(Relationship tuples)] --> Evaluate Roles[(Roles and permissions)] --> Evaluate

The public API remains narrow even when evaluation becomes sophisticated. This protects consumers from changes in storage, graph traversal, caching, or policy composition.

The diagram is deliberately logical rather than a deployment promise. The repository can evolve its processes and stores while preserving the evaluation boundary expected by callers.

Decision lifecycle

An authorization check is useful when its inputs and outputs are stable enough to inspect later. The intended lifecycle is:

sequenceDiagram participant App as Product application participant API as Aegis API participant Eval as Evaluator participant Data as Model and relationship stores participant Audit as Audit boundary App->>API: subject, action, resource, tenant/store API->>Eval: validated request Eval->>Data: load relevant model and relationships Data-->>Eval: bounded evaluation data Eval-->>API: allow/deny + safe reason trace API->>Audit: decision event API-->>App: stable decision contract

This flow separates evaluation diagnostics from the response returned to an untrusted caller. A support-facing explanation can contain more detail than a public denial without exposing the authorization graph.

Important decisions

Treat authorization as a decision, not a Boolean

Returning only true or false is convenient but loses operational context. A decision can carry a stable reason code, evaluated policy version, tenant, correlation identifier, and a safe explanation. Those details make support, auditing, and incident investigation possible without coupling callers to evaluator internals.

Make tenant context explicit

Tenant identity should not be guessed from ambient state deep inside the evaluation pipeline. Passing it through a validated request makes cross-tenant mistakes easier to test and harder to hide.

Separate policy definition from policy execution

Definitions describe intended access. The evaluator compiles that intent into a decision for a concrete subject and resource. Keeping those responsibilities separate supports versioning, review, caching, and future evaluation strategies.

Audit at the boundary

Audit records are produced where the final decision is known. Lower-level stores can emit diagnostics, but they should not independently claim that access was allowed or denied.

What it explores

The project models authorization stores, fine-grained relationships, runtime checks, graph queries, and human-readable allow or deny traces. It also treats administrative workflows and audit records as part of the product rather than incidental infrastructure.

The current work concentrates on the contracts between these pieces: how a request names a resource, how relationships are represented, how evaluators remain composable, and how audit events preserve enough context to be useful later.

The public repository currently includes the backend API, development seed data, an administrative dashboard, documentation, tests, and local deployment support. It also exposes OpenFGA-style compatibility endpoints so integration ideas can be evaluated against a familiar authorization shape.

Evidence and boundaries

Aegis is under active development. The repository is suitable for local development, demonstrations, and integration experiments; this case study does not claim production traffic, external customers, or measured scale. Current evidence is the source, tests, documentation, executable development setup, and the decision contracts that can be inspected directly.

That boundary matters for performance language. Caching and graph traversal are design concerns, but their correct limits must come from representative policy graphs and measured workloads rather than invented throughput numbers.

Trade-offs

Centralization creates consistency and observability, but it also introduces a dependency on the authorization service and its data. That makes latency, caching, failure behavior, and deployment compatibility part of the security model.

Aegis therefore prefers explicit degraded behavior over optimistic fallback. If a protected operation cannot obtain a trustworthy decision, the safe default is denial. Read-heavy paths may eventually use carefully scoped caches, but cache keys must include every dimension that can change a decision — especially tenant, subject, resource, action, and policy version.

Relationship-based access can represent richer domains than flat roles, but graph traversal is harder to reason about and operate. The project treats traversal limits and cycle handling as correctness constraints, not later optimizations.

What I am learning

  • Security boundaries become easier to maintain when they are visible in contracts.
  • Explainability is an architectural feature, not a logging afterthought.
  • Multi-tenancy changes identity, storage, caching, and observability simultaneously.
  • A centralized platform should reduce policy duplication without becoming a universal business-rules engine.
  • The most valuable tests describe denied paths and tenant isolation, not only successful access.

Why it matters

A correct answer is only half of an authorization decision. Operators and developers also need to understand which model, tuple, or rule produced it. Aegis is where I study that combination of security, multi-tenant boundaries, explainability, and production-oriented .NET architecture.

Current direction

The next milestones are to stabilize the authorization request and decision contracts, exercise tenant-isolation scenarios, and document the audit model. From there, performance work can be measured against realistic policy graphs rather than optimized in the abstract.

2026 · active

Forge

A fast, privacy-first developer workstation for the small tasks between writing code and shipping it.

TypeScript · React · Vite · Local-first

The recurring interruption

Forge brings Markdown and HTML previews, diffs, JSON formatting, JWT inspection, encoding, hashing, generators, and regex testing into a consistent interface. The problem is not that one utility is hard to find. It is that these small tasks repeatedly interrupt a larger engineering task.

Moving temporary data across unrelated websites also creates a new trust decision for each tab. Forge treats privacy, consistency, and fast return to the original work as part of the product.

Product boundary

Forge groups editors, data tools, encoders, crypto helpers, generators, and text utilities behind one navigation and interaction language. The repository is a collaborative project under the Orcace organization; this case study describes the product without implying sole authorship.

Local-first by default

Core transformations run in the browser so temporary and potentially sensitive input remains close to the device.

flowchart LR Input[Temporary developer input] --> Tool[Selected browser tool] Tool --> Transform[Local transformation] Transform --> Preview[Preview or validation] Preview --> Export[Copy or export] State[(Local workspace state)] <--> Tool Tool -. no core processing .-> Server[Remote application server]

Local-first is a product boundary, not a claim that every browser action is private. Extensions, the operating system, copied output, and future integrations have their own trust boundaries. Core tools need no account or backend processing.

Architecture and interaction model

Forge is a static Vite application built with TypeScript and React. Tool routes share shells, toolbars, copy/export behavior, keyboard patterns, theme tokens, and validation conventions. A command palette provides direct access without turning navigation into a dashboard.

The shared layer prevents each utility from inventing controls. The trade-off is that a generic shell must remain flexible enough for editors, two-pane comparisons, generators, and structured-data views.

Important decisions

Keep processing close to the input

Transformations that can run deterministically in the browser should do so. Network access is not introduced merely because deployment already has a host.

Treat examples and errors as product states

Realistic examples, precise validation, copy feedback, and recoverable errors help a utility explain itself without a separate tutorial.

Make routes durable

Each tool has a direct route, while the static host falls back to the application entry for client-side routing. A useful utility should be bookmarkable.

Trade-offs

Browser-only processing limits tasks that require protected credentials, large server-side datasets, or privileged network access. Persistent local state must avoid retaining sensitive values longer than expected. Bundle growth also needs discipline so rarely used editors do not slow every visit.

Evidence and current direction

The public repository includes the application, tests, scripts, documentation, privacy model, keyboard guide, and deployment configuration. The deployed site demonstrates the current tool catalog. No usage or performance numbers are claimed here.

The next useful work is stronger consistency, accessibility, test coverage, and careful additions where a repeated developer interruption is real. Forge represents a product principle I value: software can remove friction without becoming noisy or invasive.

2026 · active

Learn .NET

A structured path from C# fundamentals to production-ready backend engineering.

C# · .NET · ASP.NET Core · EF Core · Azure

The problem with topic lists

Learn .NET is a long-running curriculum for rebuilding knowledge from first principles. It connects language fundamentals and object-oriented design to runtime behavior, memory, concurrency, web APIs, persistence, security, architecture, testing, and delivery.

The repository exists because topic coverage is not connected understanding. Knowing C# syntax does not automatically explain allocation, cancellation, dependency lifetimes, query behavior, HTTP boundaries, or what a service needs after deployment.

Curriculum architecture

flowchart LR Language[C# and type-system fundamentals] --> Runtime[Runtime, memory, and async] Runtime --> Web[ASP.NET Core and HTTP] Web --> Data[EF Core and data boundaries] Data --> Architecture[Architecture and security] Architecture --> Operations[Testing, observability, and Azure delivery] Operations --> Projects[Integrated backend projects] Projects -. feedback .-> Language

The path is sequential enough to expose prerequisites and cyclical enough to revisit them. A production exercise often reveals that a language or runtime model was incomplete.

The learning loop

Each topic moves through understanding, implementation, explanation, and application. The goal is not to remember an API for an interview; it is to form mental models that survive framework changes.

Question -> prediction -> minimal experiment -> test -> explanation
         -> project integration -> review

The experiment isolates behavior; the project exposes interactions. A minimal async sample can explain suspension, while an API exercise reveals cancellation propagation, dependency limits, and telemetry.

Project ladder

StagePrimary evidence
Language exercisepredicted and tested behavior
Focused librarydeliberate API and error model
HTTP APItransport and application boundaries
Persistent servicequeries, transactions, migrations, concurrency
Operated workloadidentity, telemetry, deployment, recovery

Each stage introduces a constraint while keeping earlier decisions visible.

Important decisions

  • Prefer executable examples over disconnected notes.
  • Record why an abstraction exists, not only how its API is called.
  • Treat failure, cancellation, and observability as part of the exercise.
  • Keep Azure as an operational environment, not a decorative final chapter.
  • Revisit material as the supported .NET platform changes.

Boundaries and evidence

Learn .NET is an evolving repository, not a certification curriculum or a claim that every area is complete. Modules vary in maturity. Evidence is the public source, exercises, notes, and tests. Version-sensitive recommendations must be checked against the target SDK before they become current guidance.

Current direction

The repository grows toward complete backend systems: ASP.NET Core services, EF Core persistence, authentication and authorization, maintainable architecture, automated testing, and Azure delivery. The goal is one vertical slice that can be reasoned about from C# semantics through production operation.

2026 · active

Learn Rust

Rebuilding systems knowledge through ownership, algorithms, concurrency, and hands-on Rust projects.

Rust · Cargo · Algorithms · Systems Programming

Rust as a different lens

Learn Rust documents a path from ownership and borrowing to data structures, concurrency, async systems, and applications. Rust is useful not only as another language, but as a way to make memory, lifetime, and correctness decisions visible.

The repository is not an attempt to replace .NET work with a new identity. It creates a second environment for studying representation, resource lifetime, error modeling, and the cost of abstraction.

Learning architecture

flowchart LR Syntax[Syntax and core types] --> Ownership[Ownership and borrowing] Ownership --> Modeling[Enums, traits, and state modeling] Modeling --> DSA[Data structures and algorithms] DSA --> Concurrency[Concurrency and async] Concurrency --> Tools[CLI and systems projects] Tools -. reflection .-> Ownership

Ownership appears early because later topics depend on it. Data structures make representation concrete; concurrency makes shared responsibility unavoidable; projects force several models to coexist.

Learn, code, write, apply, build

The repository follows a deliberate loop: learn a concept, isolate it in code, explain it, apply it to problems, and use it in a project. Comparisons with C# connect unfamiliar mechanics to existing experience without pretending the models are identical.

Paired comparisons

Rust conceptC# comparison question
ownership and moveswhere is resource responsibility expressed?
Option<T>how is absence represented and checked?
Result<T, E>which failures belong in the type?
traitswhich behavior is static, dynamic, or constrained?
Dropwhen is deterministic cleanup guaranteed?

The goal is not to declare a winner. It is to identify which guarantees come from the compiler, runtime, API design, convention, or tests.

Competitive programming as a laboratory

Algorithm problems provide small constraints for practicing correctness, complexity, and representation. Rust adds friction around ownership and indexing that can reveal assumptions hidden by a quick implementation.

Contest and production code remain separate standards. Accepted output does not prove maintainability, robust error handling, or safe operation. A useful second pass renames intent, records invariants, and tests boundary cases.

Boundaries and evidence

Learn Rust is an evolving study path. It does not claim production deployment or benchmark superiority. Examples should compile under the recorded toolchain, and performance claims require reproducible measurement. The public repository is the evidence for completed modules; the roadmap is not evidence that later topics are already mastered.

Current direction

The longer path leads through DSA and competitive programming toward CLI tools, backend services, and small system components where performance and control matter. Success is measured by clearer questions about ownership, concurrency, data layout, and failure in any engineering environment.

2026 · active

Lens of Charlie

A static-first personal publication for writing, projects, and photography.

Astro · TypeScript · React · Cloudflare

Intent

Lens of Charlie is a calm editorial home for writing, project case studies, and photography. Its main design constraint is attention. Navigation and interaction should help a reader understand location and content without making the interface the subject.

Constraints

  • Article and project source remains portable Markdown.
  • Core reading and navigation works without a client application.
  • Interactive behavior is progressively enhanced and keyboard accessible.
  • Published slugs remain durable public identifiers.
  • Photo originals stay outside the repository.
  • Generated output is disposable and never becomes source.

Architecture

flowchart LR Writing[Writing Markdown] --> Collections[Validated content collections] Projects[Project Markdown] --> Collections Photos[Photo metadata and categories] --> Astro Collections --> Astro[Astro static build] Astro --> Pages[Cloudflare Pages] Assets[Optimized photos in GitHub] --> Astro Pages --> Reader

The repository owns writing, project source, article images, UI, and build configuration. Optimized public photographs live under public/images/photos/ in the same GitHub repository and ship with the static build. Originals stay local. A small archive with modest resolution needs no separate storage service; metadata can group photos into author-defined categories independently of their file paths.

Important decisions

Static first, not static only

Search, project previews, Mermaid diagrams, gallery layouts, lightboxes, and reading progress enhance documents that already contain semantic content. JavaScript is used where interaction earns its cost.

Content contracts before CMS features

Astro collections validate frontmatter at build time. Git provides history and review for a single-author workflow. A database or CMS becomes justified only when the product owns mutable state or authoring demonstrates a real constraint.

One visual language across different media

Inter typography, restrained contrast, outline headings, a subtle dotted field, and soft iconography connect Writing, Projects, About, and Photos. Layout width changes by content type rather than forcing every page into one container.

Trade-offs

Static generation moves work to build time. Mermaid adds an optional client chunk on pages containing diagrams. Keeping web-ready photos in GitHub grows repository history, so the archive intentionally favors a small number of optimized images over full-resolution delivery. Each trade keeps a clear boundary and can be revisited with measured evidence.

Evidence

The repository contains architecture and operating documentation, content schemas, route redirects, production link validation, responsive layouts, and accessibility-oriented progressive enhancement. Build output and internal links are checked through pnpm quality.

This remains an evolving personal publication. Real photographs, final identity copy, deeper editorial review, browser automation, and representative accessibility audits remain explicit work rather than implied completion.

Current direction

The immediate focus is content depth: stronger articles, honest project evidence, and a photo pipeline that preserves metadata intentionally without publishing private originals. Architecture should continue to make publication ordinary.