<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Casey Brooks</title>
    <description>The latest articles on DEV Community by Casey Brooks (@cjbrooks12).</description>
    <link>https://dev.to/cjbrooks12</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F66227%2F6e81450b-870b-492c-b7ad-ae964c7c0006.jpeg</url>
      <title>DEV Community: Casey Brooks</title>
      <link>https://dev.to/cjbrooks12</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cjbrooks12"/>
    <language>en</language>
    <item>
      <title>Designing a User Management system with JWTs, Ktor, and Exposed</title>
      <dc:creator>Casey Brooks</dc:creator>
      <pubDate>Fri, 11 Sep 2026 19:30:54 +0000</pubDate>
      <link>https://dev.to/cjbrooks12/designing-a-user-management-system-with-jwts-ktor-and-exposed-2j1a</link>
      <guid>https://dev.to/cjbrooks12/designing-a-user-management-system-with-jwts-ktor-and-exposed-2j1a</guid>
      <description>&lt;p&gt;Every app eventually needs to answer two questions: "who is this person?" and "what are they allowed to do?" Many tutorials either focus on integrating a specific authentication provider like Auth0 or Cognito, or else discuss low-level details like password hashing algorithms. But both of those stop short of the real question we should be asking ourselves: who actually is this person, and what does that mean in terms of how we manage their data and access? This is not just a question of 'did this person log in?', but it's a more fundamental problem of understanding the actual human interacting with your app.&lt;/p&gt;

&lt;p&gt;This may sound like a subtle distinction but it matters more than it might seem, and this post will help you understand the difference between &lt;em&gt;identity&lt;/em&gt;, &lt;em&gt;authentication&lt;/em&gt;, and &lt;em&gt;authorization&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post is part of a series on the &lt;a href="https://dev.to/cjbrooks12/series/43278"&gt;Vesper Design Diaries&lt;/a&gt;, building a Production-Grade KMP App on a Bootstrap Budget. See the &lt;a href="https://dev.to/cjbrooks12/vesper-design-diaries-building-a-production-grade-kmp-app-on-a-bootstrap-budget-4lib"&gt;Series Introduction&lt;/a&gt; for context on the Vesper app and this blog series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Identity vs Authentication vs. Authorization
&lt;/h2&gt;

&lt;p&gt;These words get often used interchangeably, but they describe fundamentally different problems. So before we dive into Vesper's user management system, let's make sure we properly understand these terms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity&lt;/strong&gt; is the simple fact of who is using the app. This is a distinctly &lt;em&gt;non-technical&lt;/em&gt; concept here. It refers to the actual human (or machine) interacting with your system, who takes actions to request and change data within it. In many cases, apps will use something like your email address to identify you as a human, since emails are something that is ubiquitous and stable. People don't change their email very often, and they are not shared, so it's a reasonable way to identify you as a person. Government systems may use something like a Social Security Number or other tax ID, but in general, the idea is that the system uses some stable String value that is uniquely associated with &lt;em&gt;you&lt;/em&gt; as the human using the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt; is identity verification: &lt;em&gt;are you who you say you are?&lt;/em&gt; When you log in, you aren't telling the system who you are. Your email, username, SSN, etc. tell the system who you are, but anyone can forge those values and claim to be you (identity theft is a real thing, y'all). What the system is actually asking for is a way to &lt;em&gt;prove&lt;/em&gt; that you are actually the person you are claiming to be. A password check, a One-Time-Password (OTP) code sent to your email or phone, a biometric scan: these are all authentication mechanisms. A strong authentication system will commonly use multiple methods of verification (MFA) by requiring you provide proof of both something you &lt;em&gt;have&lt;/em&gt; (an email address, your phone's biometrics) and something you &lt;em&gt;know&lt;/em&gt; (password, security questions).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorization&lt;/strong&gt; is permission checking: &lt;em&gt;are you allowed to do this specific thing?&lt;/em&gt; Even after you've proven who you are, the system still needs to decide whether you can read a particular resource, write to a particular table, or perform a particular action. You could walk up to the Pentagon with your real ID in hand, but the guards would not let you inside the building because you do not have permission to be there. &lt;/p&gt;

&lt;p&gt;The naive conflation of these terms works fine until it doesn't, which usually happens the first time you need to support more than one way to authenticate, revoke access without deleting the account, or you need to support different roles among users (such as system admins, moderators, and regular end-users). Having a clear differentiation between your users' identity and the way they prove that they are allowed to perform actions is something that is much easier to define at the beginning of the project than it is to bolt on later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You Still Need Your Own User System
&lt;/h2&gt;

&lt;p&gt;One common shortcut is to delegate authentication entirely to an external provider (Auth0, AWS Cognito, Google Identity) and skip building a user system at all. The provider handles login, issues JWTs, and your backend just validates the token. Simple.&lt;/p&gt;

&lt;p&gt;The problem is that the external provider only knows about authentication events. It doesn't know anything about your application's domain. It can't tell you what a user has done in your system, what device they're on, or whether a particular session has been administratively revoked by your own support team. As soon as you need to reason about users from your application's perspective (usage history, per-device push tokens, session management, merging a guest account into a registered one, allowing a user to log in with multiple social accounts), you need your own representation of a user that is independent of any specific authentication provider.&lt;/p&gt;

&lt;p&gt;The better framing is that an external OIDC provider is just one possible &lt;em&gt;source&lt;/em&gt; of authentication events. Whether a user authenticates via Auth0, passkeys, via an OTP email, personal access tokens, or as an anonymous guest, all of those are paths that eventually produce a JWT, which your backend validates in exactly the same way. The external provider shouldn't replace or become your user management system; it should just be one of a variety of ways to populate it.&lt;/p&gt;

&lt;p&gt;Vesper is designed around this from the start. It currently only uses a custom OTP authentication flow, but all of the frontend code is designed to use the same OAuth-style token management and authentication flows, so I'll be able to add Auth0 and native Google/Apple logins later without a big migration effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a JWT into the Client
&lt;/h2&gt;

&lt;p&gt;JWT stands for JSON Web Token. It's a signed JSON document: three Base64-encoded segments (header, payload, signature) joined by dots. The payload carries arbitrary claims: a subject identifier, an expiry, custom fields like &lt;code&gt;scope&lt;/code&gt;. The signature is what makes it trustworthy: whoever holds the signing secret (or the corresponding public key, for asymmetric schemes) can verify the payload hasn't been tampered with, &lt;em&gt;without contacting anyone else&lt;/em&gt;. That self-verifying property is the key that allows a JWT to be trusted as a security credential, guaranteeing the user is who they claim to be as a result of the trust we have in the system that issued the JWT.&lt;/p&gt;

&lt;p&gt;With that trust model established, the question becomes: how does the client actually obtain a JWT?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth 2.0 / OIDC&lt;/strong&gt; is the standard flow used by external providers like Auth0, Cognito, and Google. The user is redirected to the provider's login page, where they authenticate themselves to that provider, using any combination of username/password, social account logins, MFA codes, etc. After authenticating successfully with the provider, they are redirected back to your app with a deep link containing a short-lived authorization code, which is then exchanged for an access token (often JWT, but sometimes opaque) and ID token (always JWT). The resulting JWT is signed by the provider using their private key. Your backend registers a corresponding public key and from then on validation is purely local, no callback to the provider required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A hand-rolled flow&lt;/strong&gt; like email OTP works the same way with your own server as the issuer. The user submits their email, your server sends it a short-lived code, and the user presents it back proving they have access to that email inbox. Because of that verified access, your server mints a JWT directly, signed and secured by its own random secret key, so your API server alone is able to mint and verify this JWT.&lt;/p&gt;

&lt;p&gt;Both flows produce the same artifact from the client's point of view: a signed JWT to store and attach to requests.&lt;/p&gt;

&lt;p&gt;On the client, Vesper stores the access token and refresh token inside the &lt;code&gt;AppStateViewModel&lt;/code&gt; as described in the &lt;a href="https://dev.to/cjbrooks12/building-vesper-on-ballast-one-state-model-to-rule-them-all-147m"&gt;previous post&lt;/a&gt;, which persists the full state to &lt;code&gt;multiplatform-settings&lt;/code&gt; using each platform's encrypted storage. Because access tokens are short-lived (30 minutes), the client also holds a long-lived refresh token and rotates the pair silently in the background before expiry. When making an API call, we attach the token to an &lt;code&gt;Authorization: Bearer&lt;/code&gt; header, which the server then reads and verifies before calling its route handlers.&lt;/p&gt;

&lt;p&gt;Ktor's client-side &lt;code&gt;Auth&lt;/code&gt; plugin is the standard way to do this (it intercepts 401 responses and retries transparently), but I went a different direction. The &lt;code&gt;Auth&lt;/code&gt; plugin is well-suited for standard OIDC flows where the token lifecycle maps cleanly to HTTP status codes and where authentication is more-or-less a binary decision: the user is logged in, or they are not. But I wanted more explicit control over exactly when tokens get refreshed and which call sites are responsible for triggering that flow. I have the client app manually inspecting the JWT's contents and refreshing when approaching the expiry. This reduces noise in the server logs and makes a 401 mean that the user truly was not authenticated (i.e. the refresh token expired, or their session was explicitly revoked), rather than just a signal to refresh the auth token. The result looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GetMyPrayersUseCaseImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;getOrRefreshAuthTokenUseCase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GetOrRefreshAuthTokenUseCase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GetMyPrayersUseCase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PrayerDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getOrRefreshAuthTokenUseCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/protected/prayers"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer $token"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PrayerDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GetOrRefreshAuthTokenUseCase&lt;/code&gt; checks whether the stored access token is still valid by parsing its &lt;code&gt;exp&lt;/code&gt; claim directly from the Base64 payload (no library needed, it's just JSON) and returns it immediately if it has more than five minutes left. If it's close to expiry, it acquires a mutex before refreshing, re-checks inside the lock (in case another coroutine already refreshed while this one was waiting), and only then calls the refresh endpoint. The mutex prevents the thundering-herd problem where multiple concurrent requests all decide to refresh at the same moment and issue redundant calls.&lt;/p&gt;

&lt;p&gt;The explicitness at each call site is the point: it's immediately obvious which use cases touch protected APIs and which don't, and token refresh happens proactively before expiry rather than reactively on a 401. It also allows me to keep the HttpClient and the AppState completely decoupled, which is an intentional data-layer constraint that I will explain in a later post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing server routes with JWTs
&lt;/h2&gt;

&lt;p&gt;Because JWTs contain information for both identifying a user and declaring their authorized access, it's typically needed at multiple points within the API request pipeline. As a best practice, JWT authentication should be handled in multiple layers, but before getting into those details, we first need to understand the structure of a JWT.&lt;/p&gt;

&lt;h3&gt;
  
  
  The structure of a JWT
&lt;/h3&gt;

&lt;p&gt;A JWT is sent to the server in a header which looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huZG9lQGV4YW1wbGUuY29tIiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCBhcGk6cHJvdGVjdGVkIiwiaWF0IjoxNTE2MjM5MDIyfQ.S-GCY6udo4ChncUiZucu9kHIHS7OL3S48If2EeSm-IU
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When unpacked by the server, we get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Header=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWT"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Payload=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1234567890"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"johndoe@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openid profile email api:protected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1516239022&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Signature=S-GCY&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="err"&gt;udo&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;ChncUiZucu&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="err"&gt;kHIHS&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="err"&gt;OL&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;S&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="err"&gt;If&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;EeSm-IU&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header tells the server how to verify the signature, which is a core part of the JWT protocol, and the Payload contains the information the server needs to identify the user and authorize their actions. Depending on which service created the JWT, you may get a variety of claims, but at a minimum you should get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;iss&lt;/code&gt; (required): Issuer, the URL identifying the OIDC Provider&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sub&lt;/code&gt; (required): Subject, a stable unique identifier for the user at that issuer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aud&lt;/code&gt; (required): Audience, the &lt;code&gt;client_id&lt;/code&gt; the token was issued to&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;iat&lt;/code&gt; (required): Issued-at time&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exp&lt;/code&gt; (required): Expiration time&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; (common, not required): The user's full name&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;email&lt;/code&gt; (common, not required): The user's email address&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;email_verified&lt;/code&gt; (common, not required): True if the issuer has confirmed the user can access the email address, such as with a verification link&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scope&lt;/code&gt; (common, not required): A space-separated list of scopes declaring the resources this JWT permits access to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every value in the payload was verified by the issuer and signed, so your server is able to trust all of the values it contains. If the JWT claims the email was verified, you can trust that without necessarily needing to send your own email-verification link, for example. And the &lt;code&gt;scope&lt;/code&gt; list means the user is actually granting access to the corresponding resources, often by being presented with a consent screen to explicitly confirm they want to provide such information to your server.&lt;/p&gt;

&lt;p&gt;Together, all of these values constitute the result of a handshake between your server, an authorization provider, and the end user. It's a secure credential that can be treated like a password, but the "password" in this case isn't used to verify a login and initiate a session, it &lt;em&gt;is&lt;/em&gt; the session, being wholly self-contained and including all the information your server needs to accept or reject the use of any particular API endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;Ktor's &lt;code&gt;authentication&lt;/code&gt; plugin verifies the JWT is legitimate, by checking its signature against the auth provider using the &lt;code&gt;verifier&lt;/code&gt; blocks to declare which auth providers may issue JWTs to your server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;module&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="nf"&gt;authentication&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// verify guest tokens issued by our own API&lt;/span&gt;
        &lt;span class="nf"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"guestToken"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;JWT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Algorithm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HMAC256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwtConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jwtSecret&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withIssuer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwtConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jwtIssuer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withAudience&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwtConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jwtAudience&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;validate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;credential&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;JWTPrincipal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// verify tokens issued by an OIDC provider like Auth0&lt;/span&gt;
        &lt;span class="nf"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"oidcToken"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;JWT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Algorithm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RSA256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwtConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oidcPublicKey&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withIssuer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwtConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oidcIssuer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withAudience&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwtConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oidcAudience&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;validate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;credential&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;JWTPrincipal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then wrapping your API routes with the &lt;code&gt;authenticate&lt;/code&gt; route plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;routing&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"guestToken"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"oidcToken"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AuthenticationStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FirstSuccessful&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/protected/prayers"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ktor tries each verifier in order and accepts the first that succeeds. The route handler never knows which one matched; it just reads claims off the &lt;code&gt;JWTPrincipal&lt;/code&gt;. That's the key property: once inside an &lt;code&gt;authenticate&lt;/code&gt; block, identity is proven and the issuer is irrelevant. As long as &lt;em&gt;someone&lt;/em&gt; is able to vouch for the validity of the JWT and confirm it contains the necessary data for our routes, the endpoint itself really doesn't need to know anything else. The JWT will provide the stable &lt;code&gt;email&lt;/code&gt; claim regardless of whether it came from Google, Auth0, or our own OTP flow, and so we can always associate the JWT back to the user's account, since we can know the JWT came from a trusted source and could not be modified to change the email. We can trust the JWT because we trust the system that minted the JWT.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authorization
&lt;/h3&gt;

&lt;p&gt;However, proving that a JWT is legitimate is not enough. Ktor's &lt;code&gt;authenticate&lt;/code&gt; plugin does not check the content of the JWT for the allowed scopes, so we need to do that manually. You can build a basic Ktor route plugin that allows you to inspect the verified JWT payload, read its claims, and check whether the token allows access to a particular scope declared by your server. A small &lt;code&gt;scopedRoute&lt;/code&gt; helper wraps the plugin installation so each route can declare its required scope in a single line rather than calling &lt;code&gt;install(RequireScope)&lt;/code&gt; directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;
&lt;span class="c1"&gt;// Route Plugin&lt;/span&gt;
&lt;span class="c1"&gt;// ---------------------------------------------------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RequireScopeConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;RequireScope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRouteScopedPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RequireScope"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;RequireScopeConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;requiredScope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pluginConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;

    &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AuthenticationChecked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;JWTPrincipal&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RequireScope must be used with JWT authorization"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;scopes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClaim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;asString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requiredScope&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Forbidden&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Missing required scope: $requiredScope"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scopedRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requiredScope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RequireScope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requiredScope&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Apply to endpoints&lt;/span&gt;
&lt;span class="c1"&gt;// ---------------------------------------------------------------------------------------------------------------------&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configureRouting&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;routing&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"auth-jwt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;scopedRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/prayers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"prayer:read"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Here are your prayers"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{id}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Here is your prayer"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a clean two-level system on its own. But it creates a practical problem: if every route lists its own fine-grained scope, every default device token needs to enumerate all of them, which bloats the JWT, and complicates the process of adding new scopes. They typically need to be registered with the OIDC provider, and also explicitly requested by the client app, so it's best to avoid changing those scopes, if possible. &lt;/p&gt;

&lt;p&gt;One way to solve this problem is by letting the auth provider issue a JWT with a very coarse-grained scope, while the server "expands" that one scope into the full suite of actions that are enabled by it. This way, you can allow each endpoint to declare a scope that is narrow and focused on just the one task at hand, while broadly accepting the same scope and re-interpreting it to include/exclude individual endpoints as needed without needing to change the frontend or auth server code.&lt;/p&gt;

&lt;p&gt;For Vesper, this works by declaring an enum for all of the possible scopes that may be needed by an API endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppAuthTokenScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ApiProtected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api:protected"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;PrayerCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"prayer:create"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;PrayerRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"prayer:read"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;ProfileRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"profile:read"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;ProfileUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"profile:update"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;ProfileVerify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"profile:verify"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;AppAuthTokenScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;implies&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AppAuthTokenScope&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;AppAuthTokenScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ApiProtected&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;AppAuthTokenScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PrayerCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;AppAuthTokenScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PrayerRead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;AppAuthTokenScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PrayerUpdate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;AppAuthTokenScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;impliedScopes&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AppAuthTokenScope&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;implies&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;implies&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;impliedScopes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;toSet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, it is not expected that the JWT issued by the OIDC provider (or by Vesper's own OTP flow) individually lists all of these scopes. Instead, the standard scope issued is just &lt;code&gt;api:protected&lt;/code&gt;. An &lt;code&gt;impliedScopes()&lt;/code&gt; function recursively expands it to the full set of fine-grained scopes at validation time on the server, so the token stays compact while route-level checks work against the specific values. This also helps to prevent exposure of private security information to the frontend, as only the server is privy to what other scopes are implied by the &lt;code&gt;api:protected&lt;/code&gt; scope, while also allowing that set of implied scopes to change over time.&lt;/p&gt;

&lt;p&gt;With this in place, &lt;code&gt;RequireScopeConfig&lt;/code&gt; and &lt;code&gt;RequireScope&lt;/code&gt; need to be updated to work against the expanded scope list rather than raw strings. The version below replaces both from above, changing the &lt;code&gt;scope&lt;/code&gt; field from a plain &lt;code&gt;String&lt;/code&gt; to a typed &lt;code&gt;AppAuthTokenScope&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;expandScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AppAuthTokenScope&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AppAuthTokenScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstOrNull&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;impliedScopes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;toSet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nf"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RequireScopeConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppAuthTokenScope&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;RequireScope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRouteScopedPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RequireScope"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;RequireScopeConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;requiredScope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pluginConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"scope must not be null"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AuthenticationChecked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;JWTPrincipal&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RequireScope must be used with JWT authorization"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;scopes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClaim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;asString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;expandScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requiredScope&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Forbidden&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Missing required scope: $requiredScope"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configureRouting&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;routing&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"auth-jwt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;scopedRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/prayers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AppAuthTokenScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PrayerRead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Here are your prayers"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{id}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Here is your prayer"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Determining Identity
&lt;/h3&gt;

&lt;p&gt;Now that we have verified the JWT and proved they're able to access &lt;em&gt;this specific endpoint&lt;/em&gt;, we now need to figure out who exactly they are. The JWT contains claims like &lt;code&gt;sub&lt;/code&gt; and &lt;code&gt;email&lt;/code&gt; which do not necessarily match up to the equivalent values in our system, so we need a way to map the JWT to our own user DB record.&lt;/p&gt;

&lt;h4&gt;
  
  
  Database Schema
&lt;/h4&gt;

&lt;p&gt;Vesper's user management rests on three database tables with distinct responsibilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;user_profiles&lt;/code&gt;&lt;/strong&gt; is the identity record. It holds personal attributes (email, phone, name, date of birth) along with verification state, terms acceptance, and a set of OTP staging columns I'll come back to later. It knows nothing about sessions or tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;user_devices&lt;/code&gt;&lt;/strong&gt; represents a physical device (Android, iOS, Web, Desktop, or Server). Each device has a stable &lt;code&gt;device_id&lt;/code&gt; string and optionally a push token for notifications. A user profile can have many devices, though "device" is a slight misnomer, as the same physical device may have multiple records if one accesses Vesper from both the web browser and from the mobile app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;user_authorizations&lt;/code&gt;&lt;/strong&gt; is the authorization record. It links a &lt;code&gt;jwt_subject&lt;/code&gt; (the JWT &lt;code&gt;sub&lt;/code&gt; claim) to both a &lt;code&gt;user_profile_id&lt;/code&gt; and a &lt;code&gt;user_device_id&lt;/code&gt;. It also holds the refresh token, an expiry, and a &lt;code&gt;revoked&lt;/code&gt; flag. This broadly can be thought of as an "active session", and users may have multiple sessions active at once if they're logged in on different devices simultaneously.&lt;/p&gt;

&lt;p&gt;Keeping authorization separate from identity means you can revoke a session without touching the user's profile. The &lt;code&gt;revoked&lt;/code&gt; flag, combined with the &lt;code&gt;jwt_subject&lt;/code&gt; lookup on every authenticated request, gives you per-session logout, remote logout of all other devices, or a full account-wide logout, all with a targeted &lt;code&gt;WHERE&lt;/code&gt; clause against &lt;code&gt;user_authorizations&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Matching JWTs to Users
&lt;/h4&gt;

&lt;p&gt;At this point Ktor has already verified the JWT's cryptographic signature and checked the required scope. What remains is resolving the verified &lt;code&gt;sub&lt;/code&gt; claim to an active session and the identity attached to it. But "resolving" here can mean two different things depending on whether we've seen this &lt;code&gt;sub&lt;/code&gt; before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Returning user:&lt;/strong&gt; there is already a &lt;code&gt;user_authorizations&lt;/code&gt; row for this &lt;code&gt;jwt_subject&lt;/code&gt;. Look it up, run the validity checks, and return.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First login from this issuer:&lt;/strong&gt; no &lt;code&gt;user_authorizations&lt;/code&gt; row exists yet. We need to find or create the &lt;code&gt;user_profile&lt;/code&gt;, find or create the &lt;code&gt;user_device&lt;/code&gt;, wire them together in &lt;code&gt;user_authorizations&lt;/code&gt;, and then run the validity checks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The call site for route handlers is the same in both cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;ApplicationCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentUser&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;UserSession&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;JWTPrincipal&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"no JWT principal"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;jwtSubject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;  &lt;span class="c1"&gt;// the `sub` claim&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;deviceId&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"X-Device-Id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;badRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Missing device ID"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;session&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOrCreateSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwtSubject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;revoked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;unauthorized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Session has been revoked"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isExpired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;unauthorized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Session has expired"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;banned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;forbidden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Account is suspended"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;UserSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserAuthorization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// user_authorizations row&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// user_profiles row&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;UserDevice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                 &lt;span class="c1"&gt;// user_devices row&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;findOrCreateSession&lt;/code&gt; handles both paths in a single database transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;findOrCreateSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwtSubject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;JWTPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;UserSession&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Fast path: we've seen this sub before&lt;/span&gt;
    &lt;span class="nf"&gt;findSessionBySubject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwtSubject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// First login from this issuer: bootstrap the three tables&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;transaction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// The email claim lets us link to an existing profile if the user already&lt;/span&gt;
        &lt;span class="c1"&gt;// has one (e.g. they previously logged in via OTP with the same address).&lt;/span&gt;
        &lt;span class="c1"&gt;// If not, we create a fresh profile seeded from the JWT claims.&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;profile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;findProfileByEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nf"&gt;createProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;email&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;emailVerified&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emailVerified&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;device&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;findOrCreateDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;authorization&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAuthorization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;jwtSubject&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwtSubject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;userProfileId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;userDeviceId&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nc"&gt;UserSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;findSessionBySubject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwtSubject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;UserSession&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// SELECT * FROM user_authorizations&lt;/span&gt;
    &lt;span class="c1"&gt;//   JOIN user_profiles ON user_profiles.id = user_authorizations.user_profile_id&lt;/span&gt;
    &lt;span class="c1"&gt;//   JOIN user_devices  ON user_devices.id  = user_authorizations.user_device_id&lt;/span&gt;
    &lt;span class="c1"&gt;//  WHERE user_authorizations.jwt_subject = ?&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;row&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwtSubject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;UserSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUserAuthorization&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUserProfile&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUserDevice&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Why &lt;code&gt;sub&lt;/code&gt;, not &lt;code&gt;email&lt;/code&gt;, is the stable link
&lt;/h4&gt;

&lt;p&gt;Notice that the fast path queries exclusively on &lt;code&gt;jwt_subject&lt;/code&gt;, not on the email claim. This is intentional, and it's the property that lets your app and the OIDC provider evolve their data independently without breaking each other.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;sub&lt;/code&gt; claim is the OIDC provider's own stable internal ID for the user. It's issued once and never changes, even if the user changes their email address, their name, or any other profile attribute. Once &lt;code&gt;findOrCreateSession&lt;/code&gt; has run for the first time and written that &lt;code&gt;jwt_subject&lt;/code&gt; into &lt;code&gt;user_authorizations&lt;/code&gt;, the link is established permanently. Everything downstream looks up by &lt;code&gt;user_profile_id&lt;/code&gt;, which is your own stable internal ID. The external &lt;code&gt;sub&lt;/code&gt; is just the bridge between the two worlds, used exactly once per lookup, at the edge of the system.&lt;/p&gt;

&lt;p&gt;After that first login, your &lt;code&gt;user_profiles.email&lt;/code&gt; and the &lt;code&gt;email&lt;/code&gt; claim in the JWT are completely independent columns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User changes their email with the OIDC provider.&lt;/strong&gt; The &lt;code&gt;sub&lt;/code&gt; in their JWT stays the same, so &lt;code&gt;findSessionBySubject&lt;/code&gt; still resolves to the correct profile. The stale email in the JWT is irrelevant; it was only meaningful during the first-login &lt;code&gt;findProfileByEmail&lt;/code&gt; fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User changes their email inside your app.&lt;/strong&gt; &lt;code&gt;user_profiles.email&lt;/code&gt; updates normally. The JWT's &lt;code&gt;email&lt;/code&gt; claim is never consulted for an existing session, so there's no conflict and no sync required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User logs in with a second OIDC provider.&lt;/strong&gt; That provider issues a different &lt;code&gt;sub&lt;/code&gt;. &lt;code&gt;findSessionBySubject&lt;/code&gt; returns null, &lt;code&gt;findProfileByEmail&lt;/code&gt; finds the existing profile, and a second &lt;code&gt;user_authorizations&lt;/code&gt; row is inserted pointing to the same &lt;code&gt;user_profile_id&lt;/code&gt;. Now the user can log in via either provider and land on the same account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only time the JWT's identity claims (&lt;code&gt;email&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;email_verified&lt;/code&gt;) matter is during that first-login bootstrap. After that your profile is the authority on identity, and the OIDC server is the authority on authentication. Neither needs to know what the other is doing.&lt;/p&gt;

&lt;p&gt;With that plumbing in place, route handlers reduce to a single call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/protected/prayers"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;prayers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPrayers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ownerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prayers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether the JWT came from the OTP flow, an OIDC provider, or a Personal Access Token, the call is identical; the issuer difference was resolved upstream by the Ktor verifier, and the identity/session link was resolved by &lt;code&gt;findOrCreateSession&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tying it all together
&lt;/h2&gt;

&lt;p&gt;Here is a high-level walkthrough of a single authenticated request, from the moment the client decides to make a call to the moment the route handler runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Client-side Flow
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Step 1: The client checks whether it has any tokens at all.&lt;/strong&gt; If no tokens are in encrypted storage, the user is redirected to the appropriate auth flow to obtain a fresh token pair. Everything below assumes that check passed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: The client checks whether the access token is still usable.&lt;/strong&gt; &lt;code&gt;GetOrRefreshAuthTokenUseCase&lt;/code&gt; reads the &lt;code&gt;exp&lt;/code&gt; claim directly from the stored access token. If it has more than five minutes of lifetime remaining, it's returned immediately and the request proceeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: If the access token is expiring, the client silently refreshes.&lt;/strong&gt; A coroutine mutex gates the refresh call so that only one concurrent request actually hits the refresh endpoint. If the refresh fails, the client clears its tokens and sends the user back to the auth flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: The client makes a request with the token in the header.&lt;/strong&gt; The token is attached to &lt;code&gt;Authorization: Bearer {accessToken}&lt;/code&gt; and sent. From the client's perspective, that's all it needs to know: the token either works or it doesn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server-side Flow
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Ktor receives a request.&lt;/strong&gt; The server matches it to a route and begins running the request pipeline for that route.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Ktor's &lt;code&gt;authenticate&lt;/code&gt; block runs.&lt;/strong&gt; The JWT's signature is verified against the registered verifiers. A non-matching or expired token returns a 401 immediately; the route handler is never touched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: The &lt;code&gt;RequireScope&lt;/code&gt; plugin runs.&lt;/strong&gt; The &lt;code&gt;scope&lt;/code&gt; claim is expanded through &lt;code&gt;expandScope()&lt;/code&gt; and checked against the scope required by this specific route. A missing scope returns a 403.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: The route handler calls &lt;code&gt;getCurrentUser()&lt;/code&gt;.&lt;/strong&gt; The &lt;code&gt;sub&lt;/code&gt; claim is resolved to a &lt;code&gt;UserSession&lt;/code&gt; via &lt;code&gt;findOrCreateSession&lt;/code&gt;, automatically provisioning profile and device records on first login from a new issuer. The session is then checked for revocation, expiry, and user bans before proceeding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: The route handler does its work.&lt;/strong&gt; With a fully resolved &lt;code&gt;UserSession&lt;/code&gt; in hand, the handler performs its work using &lt;code&gt;user.userProfile.id&lt;/code&gt; as the stable owner identifier, completely unaware of which auth provider issued the token or how the identity was resolved.&lt;/p&gt;

&lt;p&gt;The consistent principle across all of this is that identity, authentication, and authorization are kept separate not just as concepts but as code. Each layer owns one check and passes a stronger guarantee to the next. The result is a pipeline that works identically whether the JWT came from a hand-rolled OTP flow, an OIDC provider, or any future auth mechanism, because by the time the route handler runs, the token has already been forgotten. Each system is self-contained, doesn't leak implementation details into the next layer, and is able to evolve on its own without impacting the rest of the application. &lt;/p&gt;

&lt;p&gt;Together, all of this builds a stable foundation upon which we can build the rest of the functionality needed for a complex system.&lt;/p&gt;

&lt;h2&gt;
  
  
  PS: A note on opaque tokens
&lt;/h2&gt;

&lt;p&gt;Everything in this post assumes the auth provider issues JWTs for access tokens, which is the most common case and the default for many providers. However, some OIDC providers (Auth0 being a notable example) can be configured to issue opaque access tokens instead: short random strings with no parseable payload.&lt;/p&gt;

&lt;p&gt;Opaque tokens require slightly different mechanisms for checking expiry on the client and for resolving the user's identity and scope on the server. But those differences are implementation details. The underlying concepts are identical: the client still needs to know when a token is approaching expiry and refresh it proactively, and the server still needs to verify the token, resolve the authorized scope, and map it to a stable user identity before the route handler runs. The shape of the pipeline doesn't change, only the specific tools used at each step.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>multiplatform</category>
      <category>ktor</category>
      <category>shipaton</category>
    </item>
    <item>
      <title>Building Vesper on Ballast: One State Model to Rule Them All</title>
      <dc:creator>Casey Brooks</dc:creator>
      <pubDate>Thu, 03 Sep 2026 16:57:21 +0000</pubDate>
      <link>https://dev.to/cjbrooks12/building-vesper-on-ballast-one-state-model-to-rule-them-all-147m</link>
      <guid>https://dev.to/cjbrooks12/building-vesper-on-ballast-one-state-model-to-rule-them-all-147m</guid>
      <description>&lt;p&gt;As mentioned in the &lt;a href="https://dev.to/cjbrooks12/vesper-design-diaries-building-a-production-grade-kmp-app-on-a-bootstrap-budget-4lib"&gt;Series Introduction&lt;/a&gt;, Ballast is a foundational piece of the architecture of the Vesper app. Ballast is &lt;em&gt;the&lt;/em&gt; architecture of the frontend application, and it is the Kotlin-language interface to the server-side schedules and background queue workloads. An understanding of Ballast and the problems it was designed to solve are crucial for understanding why I built Vesper in the way I did. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post is part of a series on the &lt;a href="https://dev.to/cjbrooks12/series/43278"&gt;Vesper Design Diaries&lt;/a&gt;, building a Production-Grade KMP App on a Bootstrap Budget. See the &lt;a href="https://dev.to/cjbrooks12/vesper-design-diaries-building-a-production-grade-kmp-app-on-a-bootstrap-budget-4lib"&gt;Series Introduction&lt;/a&gt; for context on the Vesper app and this blog series.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Full disclosure before anything else: I wrote Ballast. I've been building it for several years, and Vesper is the most complete application I've built with it. Take my recommendation with appropriate skepticism — I'm clearly not a neutral party. That said, Vesper is the real test of whether my own ideas hold up under production conditions, and I think they do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ballast Is and Why I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/copper-leaf/ballast" rel="noopener noreferrer"&gt;Ballast&lt;/a&gt; started out as a KMP MVI framework, essentially a take on the Redux library popular with React. But Ballast takes the general idea of Redux and expands it to leverage some really nice features of the Kotlin language (like sealed interfaces), while allowing more flexibility to the overall "unidirectional data flow" programming model though the use of Coroutines, all without sacrificing any of the safety that this pattern was created for. Ballast is a highly opinionated library that aims to give you a structure for your code that is easily repeatable, yet adaptable for each individual application.&lt;/p&gt;

&lt;p&gt;So what does Ballast look like? The short version: every Screen defines a &lt;strong&gt;Contract&lt;/strong&gt; — a sealed interface of &lt;code&gt;Inputs&lt;/code&gt; (things that can happen), a &lt;code&gt;State&lt;/code&gt; (the current snapshot), and &lt;code&gt;Events&lt;/code&gt; (one-shot side effects like navigation or dialogs). In practice with a pure KMP Compose app, Events are not really used (they are much more necessary with working with legacy Android code). But here's what a Contract in Ballast typically looks like, describing the functionality of a Stopwatch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;elapsedSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;isRunning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Inputs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Start&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Inputs&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Stop&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Inputs&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Reset&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Inputs&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Tick&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Inputs&lt;/span&gt; &lt;span class="c1"&gt;// posted internally when the stopwatch timer is running&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Events&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Contract is just a statement of the data that can be displayed to the screen (the State), and the Inputs are the actions the UI and background processes dispatch to change the State. An &lt;code&gt;InputHandler&lt;/code&gt; processes each Input and returns an updated State or schedules additional work, while a &lt;code&gt;ViewModel&lt;/code&gt; wires them together with a &lt;code&gt;CoroutineScope&lt;/code&gt; to control its lifetime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StopwatchInputHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;InputHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;
        &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;InputHandlerScope&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;
            &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Start&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;updateState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRunning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nf"&gt;sideJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tick"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nf"&gt;postInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Stop&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;updateState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isRunning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nf"&gt;cancelSideJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tick"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cm"&gt;/* cancels the running loop */&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Reset&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;updateState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elapsedSeconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isRunning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Tick&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;updateState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elapsedSeconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;elapsedSeconds&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StopwatchViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;coroutineScope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CoroutineScope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BasicViewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;
        &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;coroutineScope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coroutineScope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BallastViewModelConfiguration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;inputHandler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StopwatchInputHandler&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;initialState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"StopwatchViewModel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;also&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;inputStrategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FifoInputStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;typed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;eventHandler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;eventHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Compose, it's best to define each screen with two components: one which is fully Stateless that renders content purely based on the current State, and a Stateful version which gets the ViewModel instance via Dependency Injection and coordinates state and inputs with the Stateless version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Stateful — acquires the ViewModel and bridges to the stateless component&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;StopwatchScreen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;vm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;koinViewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StopwatchViewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;state&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observeStates&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;collectAsState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nc"&gt;StopwatchScreenContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;postInput&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trySend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Stateless — receives State and a postInput lambda; no ViewModel reference needed&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;StopwatchScreenContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;postInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;horizontalAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CenterHorizontally&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${state.elapsedSeconds}s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;typography&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;displayLarge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nc"&gt;Row&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isRunning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;postInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;postInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Start"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nc"&gt;OutlinedButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;postInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StopwatchContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Reset"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll notice that none of the above code requires annotation processors, code generation, or reflection to function. Ballast has none of that. Every contract, handler, and adapter is code I wrote and can navigate to directly in IntelliJ. When something breaks at 2am, I'm not guessing what a generated class is doing — I'm just reading the actual implementation (or nowadays, having Claude read it).&lt;/p&gt;

&lt;p&gt;The "no magic" constraint isn't just aesthetics. On Kotlin Multiplatform, code generation support across all targets is still uneven. Reflection is either unavailable or slow on Native. KSP requires additional Gradle boilerplate and longer compile times, and makes it difficult to find, debug, and modify the generated code. Ballast sidesteps those problems entirely by requiring explicit code. You write more boilerplate upfront, but you get a consistent, debuggable, fully-navigable codebase in return, which can be freely modified or extended without requiring changes to the Ballast library itself. &lt;/p&gt;

&lt;p&gt;Historically, the boilerplate problem was solved with scaffold tools, but these days, AI agents are so good at following patterns that you really don't even need that. I used Claude Code to build much of Vesper, and I was honestly shocked at how well it was able to follow the Ballast boilerplate without explicit prompting, given that Ballast is a bespoke library without much adoption. As a coworker told me when first getting introduced to Claude, "Claude loves structure". I completely agree, and Ballast's opinionated nature works with that very well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Contract Pattern in Practice
&lt;/h2&gt;

&lt;p&gt;A typical screen in Vesper — such as the Submit Prayer screen — defines a contract like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;SubmitPrayerScreenContract&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;prayerText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;isSubmitting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="c1"&gt;// many properties can be derived from the state, and it's best to &lt;/span&gt;
        &lt;span class="c1"&gt;// co-locate that here to keep the logic out of Compose&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;isPrayerValid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prayerText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Inputs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;UpdatePrayerText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Inputs&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Submit&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Inputs&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Events&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;InputHandler&lt;/code&gt; is a pure function that handles Inputs one-at-a-time (though a Coroutine Channel) and provides APIs&lt;br&gt;
to update the state, start "side jobs" (coroutines that run the "background" of the ViewModel), or send Events or &lt;br&gt;
follow-up Inputs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SubmitPrayerScreenInputHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;submitPrayer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SubmitPrayerUseCase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;InputHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;InputHandlerScope&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UpdatePrayerText&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;updateState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prayerText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Submit&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;currentState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;updateStateAndGet&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isSubmitting&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;prayerResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;submitPrayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prayerText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;updateState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isSubmitting&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nf"&gt;sideJob&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendAndAwaitCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GoToDestination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/prayers/${prayerResult.id}"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;updateState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isSubmitting&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that unlike Redux, Ballast does not require a strict 1:1 transformation from the Input to a State. Instead, it just carefully guards the internal StateFlow so that a single Input may safely use Coroutines to make several calls in series, updating the state as it goes. It also ensures Inputs are queued up and processed one-at-a-time in the order they were received, making sure there are no race conditions with your InputHandler code.&lt;/p&gt;

&lt;p&gt;Since the InputHandler may have dependencies injected via DI, the InputHandler itself must be injected into a &lt;code&gt;ViewModel&lt;/code&gt;'s constructor, along with a &lt;code&gt;CoroutineScope&lt;/code&gt; to define the lifetime of the ViewModel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SubmitPrayerScreenViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;coroutineScope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CoroutineScope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;inputHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SubmitPrayerScreenInputHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;configurationBuilder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BallastViewModelConfiguration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BasicViewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;coroutineScope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coroutineScope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configurationBuilder&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputHandler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initialState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SubmitPrayerScreenContract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The coroutine scope comes in from outside. That's the key design choice: whoever provides the scope owns the ViewModel's lifetime. On a screen, that's the navigation backstack entry or the Compose hierarchy (via &lt;code&gt;rememberCoroutineScope()&lt;/code&gt;). For app-wide state, it's the Ktor application lifecycle on the server, or an application-scoped scope on mobile. I'll cover the backstack lifetime management in a dedicated post on navigation. I also have a Koin &lt;code&gt;factory { }&lt;/code&gt; function which defines a base &lt;code&gt;BallastViewModelConfiguration.Builder&lt;/code&gt;, which add cross-cutting Interceptors into all ViewModels, so I get things like automatic logging and a Debugger attached by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application-Wide State
&lt;/h2&gt;

&lt;p&gt;ViewModels don't need to be limited to just the UI layer, though. Vesper uses an additional &lt;code&gt;AppStateViewModel&lt;/code&gt; scoped to an application-wide CoroutineScope which holds the state that needs to outlive any individual screen: auth tokens, theme preference, onboarding status, feature flags. This ViewModel uses a &lt;code&gt;@Serializable&lt;/code&gt; State class with the &lt;a href="https://github.com/copper-leaf/ballast/tree/main/ballast-saved-state" rel="noopener noreferrer"&gt;saved-state module&lt;/a&gt; to persist all changes to &lt;code&gt;multiplatform-settings&lt;/code&gt;. Cold launches restore the state from disk rather than starting blank, and the whole application is gated on this state restoration in the Splash Screen, so we can ensure it was correctly restored before attempting to use any of the AppState data.&lt;/p&gt;

&lt;p&gt;For an online-first app like Vesper, this pattern acts as a lightweight data layer that sidesteps the need for a full SQLite database. The app's local storage needs are modest — a handful of preference values and session tokens — and a single serialized &lt;code&gt;State&lt;/code&gt; covers all of it without a schema, migrations, a query layer, or DAO mapping code. Auth tokens and other sensitive fields are stored using encrypted storage helpers, making the settings store a safe credential store. Because Ballast processes Inputs sequentially, callers can use &lt;code&gt;sendAndAwaitCompletion&lt;/code&gt; to post an Input and suspend until it has been fully handled — including until &lt;code&gt;BallastSavedStateInterceptor&lt;/code&gt; has flushed the new state to disk. The ViewModel acts as a fast in-memory cache; &lt;code&gt;sendAndAwaitCompletion&lt;/code&gt; gives you the guarantee that the cache and the store are in sync when you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Same Pattern on the Server
&lt;/h2&gt;

&lt;p&gt;Beyond UI and application state, Ballast also powers the server-side background queues (via &lt;a href="https://github.com/copper-leaf/ballast/tree/main/ballast-queue-core" rel="noopener noreferrer"&gt;ballast-queue&lt;/a&gt;) and cron-style scheduled jobs (via &lt;a href="https://github.com/copper-leaf/ballast/tree/main/ballast-scheduler-core" rel="noopener noreferrer"&gt;ballast-scheduler&lt;/a&gt;) — the same Contract/InputHandler model, just with Inputs serialized to a Postgres table and retried on failure rather than processed in memory. Navigation is the same story: &lt;a href="https://github.com/copper-leaf/ballast/tree/main/ballast-navigation" rel="noopener noreferrer"&gt;ballast-navigation&lt;/a&gt; drives routing with a URL-based backstack, using the same Contract pattern you saw above. I'll cover all of these topics in their own posts; the point for now is that learning the Contract/InputHandler shape once gives you a mental model that applies across the entire application stack and across various domains, not just UI state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters for Vesper
&lt;/h2&gt;

&lt;p&gt;Vesper is a relatively simple app, and yet still has around 30 screens to manage, along with an application-wide state machine, non-trivial navigation logic, and a lot of complex platform-specific logic needed to bridge everything together. All of this runs within the Ballast system, making all user interactions and state changes predictable and reliable. New developers (or AI agents) know exactly what to look for in any feature: find the Contract, read the Inputs to know what can happen, read the InputHandler to know how those changes get applied.&lt;/p&gt;

&lt;p&gt;The consistency also makes testing straightforward. &lt;code&gt;InputHandler&lt;/code&gt; implementations have no framework dependencies — they take domain use cases as constructor arguments, which can be stubbed for tests. The State is a plain data class, which allows us to recreate any possible UI state for previews or snapshot tests. There's nothing to mock at the Ballast layer itself. I can verify the full behavioral logic of any screen by constructing its InputHandler, feeding it Inputs, and asserting on the resulting State — no Compose runtime, no Android emulator, no network.&lt;/p&gt;

&lt;p&gt;That same legibility extends to AI-assisted development. Because each screen's logic is fully self-contained in a &lt;code&gt;Contract&lt;/code&gt; + &lt;code&gt;InputHandler&lt;/code&gt; pair, Claude could confidently generate or modify an InputHandler for a screen it had never seen before, just by pattern-matching against the rest of the codebase. The explicit structure that Ballast requires turns out to be exactly the kind of structure that makes a codebase easily understandable to both humans and machines.&lt;/p&gt;

&lt;p&gt;I'll be referring back to Ballast throughout this series. The routing system, the server-side queue, and the saved-state adapter are all built on it. Understanding the basic Contract/InputHandler/ViewModel shape is the prerequisite for all of the other posts. You can find more documentation and examples, as well as additional features not used by Vesper such as Undo/Redo support or Firebase integrations, at &lt;a href="https://github.com/copper-leaf/ballast" rel="noopener noreferrer"&gt;github.com/copper-leaf/ballast&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>multiplatform</category>
      <category>compose</category>
    </item>
    <item>
      <title>Vesper Design Diaries: Building a Production-Grade KMP App on a Bootstrap Budget</title>
      <dc:creator>Casey Brooks</dc:creator>
      <pubDate>Thu, 03 Sep 2026 16:25:06 +0000</pubDate>
      <link>https://dev.to/cjbrooks12/vesper-design-diaries-building-a-production-grade-kmp-app-on-a-bootstrap-budget-4lib</link>
      <guid>https://dev.to/cjbrooks12/vesper-design-diaries-building-a-production-grade-kmp-app-on-a-bootstrap-budget-4lib</guid>
      <description>&lt;p&gt;On September 3rd I launched &lt;a href="https://vesperapp.cc" rel="noopener noreferrer"&gt;Vesper&lt;/a&gt;, a cross-platform Christian prayer app for Android and iOS, as my entry in the &lt;a href="https://shipaton.com" rel="noopener noreferrer"&gt;RevenueCat 2026 Shipaton&lt;/a&gt;. This is not just a basic app thrown together quickly for this context, though: I have spent months building it and iterating on the features and design, as I am building it as a real production-grade application to bootstrap a business. I'm publishing this series of technical posts as a retrospective look back on how I built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bit of context
&lt;/h2&gt;

&lt;p&gt;I first got the idea for this app and started working on Vesper in early May. The Shipaton happened to coincide with my already-planned release schedule, so I decided to wait a few extra weeks so I could polish things up and document the technical decisions I'd made along the way for the benefit of the Kotlin community. These posts are written &lt;em&gt;after&lt;/em&gt; the fact. I'm not live-blogging the build; I'm looking back at what I built and explaining the 'why' behind my decisions.&lt;/p&gt;

&lt;p&gt;Vesper was designed heavily around &lt;a href="https://github.com/copper-leaf/ballast" rel="noopener noreferrer"&gt;Ballast&lt;/a&gt;, a Kotlin Multiplatform state management library I have maintained for many years. Ballast is mostly my personal opinionated structure for building things like per-screen UI state and app navigation, so part of my motivation for this series is showing what Ballast looks like in a real, production-scale application. &lt;/p&gt;

&lt;p&gt;But the things I learned building Vesper apply to anyone looking to make a real-world, production-ready application today, whether you choose to use Ballast or not. While these blogs will describe the actual code of Vesper using Ballast, the general knowledge and ways to think about building software capable of handling production workloads are not specific to Ballast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who these posts are for
&lt;/h2&gt;

&lt;p&gt;This is an advanced series. I'm not going to walk through setting up a Ktor project from scratch or explain what a coroutine is. What I &lt;em&gt;will&lt;/em&gt; do is focus on higher-level architectural decisions and the tradeoffs that come with them — the kind of thinking that goes into building software you actually intend to maintain and grow over many years.&lt;/p&gt;

&lt;p&gt;Vesper is not a throwaway pet project. It's the foundation of what I hope can eventually become a real company. That shapes every decision in the codebase.&lt;/p&gt;

&lt;p&gt;That said, I want to be honest about the audience for these posts: most of what I describe here is almost certainly &lt;em&gt;over-architected&lt;/em&gt; for a simple side project, and quite probably &lt;em&gt;under-powered&lt;/em&gt; for an established company with a real engineering team and real infrastructure budget. The constraint I was working under is the one you find at an early-stage pre-launch startup — needing production-stable code without being able to pay for production-grade services. I built a lot of things myself that I could have paid for as a service (email campaigns, feature flags, push notifications, job queues) not just because I couldn't afford those services yet, but also because I don't yet need many of the advanced features those services provide. That context matters when reading these posts.&lt;/p&gt;

&lt;p&gt;That said, the ability to tailor these features to exactly my needs without having to compromise on the available features or uptime of another service is a distinct benefit that I will not throw away lightly. I did not build features like job queues or email marketing directly into Vesper just because I want to save money. By building them myself, I am able to evolve the features and functionality along with the product itself in a way that I would not be able to with a SaaS product. It will be slower, for sure, but that's a luxury that I can afford now, and that I believe will continue to serve Vesper well far into the future and lead to an application that is every bit as stable as one built on SaaS products, but at a fraction of the monthly cost.&lt;/p&gt;

&lt;p&gt;With Vesper, I find myself working in a space that I have not found to be documented very well, because most content you'll find online is focused on absolute beginners, or large enterprise customers. It's actually pretty hard to bridge the gap between the two, but I hope to be able to give a glimpse into what it looks like working in the space between those two extremes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kotlin Multiplatform is not optional here
&lt;/h2&gt;

&lt;p&gt;The entire application is enabled by Kotlin Multiplatform. Vesper in its current form — a single codebase running across Android, iOS, and eventually web, with domain logic, contracts, and data models shared between the Compose frontend and the Ktor backend — could not exist without it. Some of these posts will focus heavily on specific Kotlin language features or KMP-specific patterns. Others will spend more time on the problem itself and less on the Kotlin mechanics. Either way, I'll make sure there's at least one concrete Kotlin snippet in every post so it's grounded in something real.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the source code
&lt;/h2&gt;

&lt;p&gt;Vesper is a closed-source application. The code I share in these posts is the only direct look at the internals you'll get. That said, nothing in the repo is a genuine trade secret — it's just a mobile app, and a relatively low-risk one at that. Several of the components I built are self-contained enough that I'd happily extract and publish them as open-source libraries if there's enough interest. I'll call those out explicitly as I go, and I'll be providing code snippets where relevant. But because most of the code is currently closed-source, don't expect to be able to copy-and-paste any of these code snippets into your project and expect it to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Roadmap
&lt;/h2&gt;

&lt;p&gt;The series will cover many topics cross the full spectrum of a full-stack Kotlin Multiplatform app, including Compose frontend, Ktor+Exposed backend, and infrastructure. &lt;/p&gt;

&lt;p&gt;I will update this list with links and new entries as I publish the posts, so bookmark this page or follow me on &lt;a href="https://dev.to/cjbrooks12"&gt;Dev.to&lt;/a&gt; if you want to follow along!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/cjbrooks12/building-vesper-on-ballast-one-state-model-to-rule-them-all-147m"&gt;Building Vesper on Ballast: One State Model to Rule Them All&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Designing a User Management system with JWTs, Ktor, and Exposed&lt;/li&gt;
&lt;li&gt;Theming and Dynamic Font Loading in Compose&lt;/li&gt;
&lt;li&gt;Deep-Linking and Navigation with KMP&lt;/li&gt;
&lt;li&gt;Domain-Driven Development - The Secret Weapon for Maintainable Apps&lt;/li&gt;
&lt;li&gt;Contract-Based Development for Sharing Code between Compose and Ktor&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Is Vesper?
&lt;/h2&gt;

&lt;p&gt;While I am not writing these blog posts as marketing for the app itself, it may be helpful for you to have a bit of insight into the core functionality of the app. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Vesper is a prayer app and a community of Believers carrying each other's burdens. We gather 2 or 3 to join with Jesus in prayer, building a daily rhythm of intercession together.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Vesper works on a system of matching submitted prayers anonymously to other users ready to pray for them. It's intentionally &lt;em&gt;not&lt;/em&gt; a social network as there is no concept of following, liking, or replying to prayers. It's just a place for Christians to ask for prayer in their time of need, and for other Christians to build a stronger habit of prayer by interceding for those requests.&lt;/p&gt;

&lt;p&gt;If you're a follower of Jesus and would like to join the community, you can sign up for the mailing list or find links to download the app and join the community at &lt;a href="https://vesperapp.cc/" rel="noopener noreferrer"&gt;vesperapp.cc&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;But regardless of what you believe, I hope this deeper look into what it took to build Vesper will be helpful and a blessing to you!&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>multiplatform</category>
      <category>compose</category>
    </item>
    <item>
      <title>0.21.0 Released</title>
      <dc:creator>Casey Brooks</dc:creator>
      <pubDate>Mon, 01 Jun 2020 18:47:50 +0000</pubDate>
      <link>https://dev.to/orchidhq/0-21-0-released-2cb6</link>
      <guid>https://dev.to/orchidhq/0-21-0-released-2cb6</guid>
      <description>&lt;p&gt;Being stuck at home may not be the most exciting, but it has given me lots of time to fix bugs and make Orchid better
for you! This latest release, 0.21.0, includes many bug fixes identified (and sometimes fixed!) by the community, and
also boasts some significant internal changes which improve Orchid's overall performance and prepare a major portion of
its functionality to be pulled into a standalone library (which you'll absolutely want to check out when it's released).&lt;/p&gt;

&lt;p&gt;This is the official Orchid newsletter, the newest and best documentation site generator. There is a growing need to
keep the community up-to-date on all the happenings around Orchid, and here I will share Orchid's progress, milestones,
and future plans! Follow along with this series to stay on top of Orchid's newest features, track adoption on Github,
and see who's using Orchid!&lt;/p&gt;

&lt;h2 id="on-github"&gt;
On Github&lt;/h2&gt;

&lt;p&gt;The Orchid community is growing larger all the time, and the repo is now at 367 stars!&lt;/p&gt;

&lt;p&gt;Orchid also received contributions from &lt;a href="https://github.com/vmj" rel="noopener noreferrer"&gt;Mikko Värri&lt;/a&gt; for improvements to sitemaps, and
&lt;a href="https://github.com/JavierSegoviaCordoba" rel="noopener noreferrer"&gt;Javier Segovia Córdoba&lt;/a&gt; with documentation fixes.&lt;/p&gt;

&lt;p&gt;And thank you to everyone else who has been giving Orchid a try, reaching out with questions and feedback, and generally
supporting this growing community!&lt;/p&gt;

&lt;h2 id="whats-new"&gt;
What's New?&lt;/h2&gt;

&lt;p&gt;Full release notes and a migration guide are available &lt;a href="/changelog"&gt;here&lt;/a&gt;. Here's a summary of what to
expect in 0.21.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Experimental sourcedocs are now enabled by default and do not need a CLI flag to use.&lt;/li&gt;
&lt;li&gt;Sourcedocs modules can now cross-link between each other for multi-module projects, or those with multiple interlinked
sourcesets (like Kotlin MPP).&lt;/li&gt;
&lt;li&gt;Collections have become the base unit of organizing groups of pages in Orchid, and there is now a &lt;code&gt;collectionPages&lt;/code&gt;
menu item to display pages from a collection from any plugin.&lt;/li&gt;
&lt;li&gt;Asset management is greatly improved, and support for downloading and inlining CSS and JS assets is now available from
&lt;code&gt;config.yml&lt;/code&gt; as well as from plugin code.&lt;/li&gt;
&lt;li&gt;Diagnostic output across the board is generally improved. Pebble templates are now more descriptive about errors with
better line-number tracking (to aid in resolving specific Pebble template issues), and &lt;code&gt;OrchidTest&lt;/code&gt; now only shows
HTML selectors from the test instead of the entire rendered page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id="in-progress"&gt;
In Progress&lt;/h1&gt;

&lt;p&gt;Continued work is underway to support relative URLs (for viewing an Orchid site without needing a local fileserver). In
addition, Orchid's resource/file management is a core subsystem that would do well to be a separate library, and work is
in progress to move it out of Orchid and into Arcana, it's own library. This library will be Kotlin
multiplatform-enabled, bringing Orchid's simple and flexible file-management APIs to Android, iOS, JS, and of course,
pure JVM.&lt;/p&gt;





&lt;p&gt;Are you interested in getting started with Orchid? There simply is no better way to manage all the documentation for
your project, and I'd love to help you get set up!&lt;/p&gt;

&lt;p&gt;If you have an open-source project that needs docs, are building a new portfolio, or are building any other kind of
static site, I want to work with you to get you set up with Orchid! Comment on this post, send me a PM here on Dev.to,
reach out on &lt;a href="https://gitter.im/JavaEden/Orchid" rel="noopener noreferrer"&gt;Gitter&lt;/a&gt;, or &lt;a href="https://www.caseyjbrooks.com/contact/" rel="noopener noreferrer"&gt;contact me here&lt;/a&gt;
and I will be with you every step of the way.&lt;/p&gt;

&lt;p&gt;And as always, let me know if you start using Orchid so I can feature you in the next update!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>orchid</category>
      <category>jamstack</category>
      <category>webdev</category>
    </item>
    <item>
      <title>New Snippets plugin, 0.20.0 Released</title>
      <dc:creator>Casey Brooks</dc:creator>
      <pubDate>Mon, 30 Mar 2020 17:33:59 +0000</pubDate>
      <link>https://dev.to/orchidhq/new-snippets-plugin-0-20-0-released-1m32</link>
      <guid>https://dev.to/orchidhq/new-snippets-plugin-0-20-0-released-1m32</guid>
      <description>&lt;p&gt;The next major version of Orchid, 0.20.0, is now available! It includes the first introduction of the long-awaited
&lt;a href="/plugins/orchidsnippets"&gt;Snippets&lt;/a&gt; plugin, as well as a number of smaller quality-of-life improvements throughout.
Check out the full release notes &lt;a href="/changelog"&gt;here&lt;/a&gt;, and stick around for a general update of the
development and community around Orchid!&lt;/p&gt;

&lt;p&gt;This is the official Orchid newsletter, the newest and best documentation site generator. There is a growing need to
keep the community up-to-date on all the happenings around Orchid, and here I will share Orchid's progress, milestones,
and future plans! Follow along with this series to stay on top of Orchid's newest features, track adoption on Github,
and see who's using Orchid!&lt;/p&gt;

&lt;h2 id="on-github"&gt;
On Github&lt;/h2&gt;

&lt;p&gt;Orchid is now at 343 stars on Github, and since the last newsletter has received a PR from
&lt;a href="https://github.com/BrunoVernay" rel="noopener noreferrer"&gt;Bruno Vernay&lt;/a&gt; with some improvements to the formatting of blog post filenames.&lt;/p&gt;

&lt;p&gt;I also want to give a special thanks to everyone who provided feedback and suggestions for the new Snippets plugin! Both
on the proposal issue and on Gitter, your suggestions are what decided the features to include in this first release,
and it's my hope that it will provide exactly the solution you're looking for!&lt;/p&gt;

&lt;p&gt;And thank you to everyone else who has been giving Orchid a try, reaching out with questions and feedback, and generally
supporting this growing community!&lt;/p&gt;

&lt;h2 id="whats-new"&gt;
What's New?&lt;/h2&gt;

&lt;p&gt;This latest release is a continuation of the "quality-of-life" improvements that have been suggested by the you and are
making Orchid better and easier to use. Some of these changes required breaking the internal API (thus the major version
bump to 0.20.0), but this version does not include any deprecations or breaking changes to the public, end-user
features. Below are the new features available in 0.19.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New Snippets plugin allows you to lookup and embed snippets from code, files, and even external webpages!&lt;/li&gt;
&lt;li&gt;The Admin Panel layout got a minor makeover to make it easier to use and more flexible.&lt;/li&gt;
&lt;li&gt;Forms and snippets located during the site build are care listed and can be previewed in the admin panel.&lt;/li&gt;
&lt;li&gt;Allows Forms to be embedded with a new &lt;code&gt;form&lt;/code&gt; tag, instead of only as Components as before.&lt;/li&gt;
&lt;li&gt;Replaces default accordion markup with details/summary HTML5 tag for best usage in all themes&lt;/li&gt;
&lt;li&gt;Tabbed TemplateTags can now be rendered dynamically! You can now use loops and conditionals to add individual tabs to
the body of tabbed tags, instead of having to have them be statically-determined.&lt;/li&gt;
&lt;li&gt;Both the base URL and your theme can now be set in &lt;code&gt;config.yml&lt;/code&gt;, instead of needing to be set in your Gradle or Maven
build scripts.&lt;/li&gt;
&lt;li&gt;Improvements to base URL management now allow plugins to provide "helpers" for determining the base URL, such as
OrchidNetlify detecting when Orchid is running on Netlify's CI platform and using its build environment variables
to find the base URL.&lt;/li&gt;
&lt;li&gt;OrchidPosts now relaxes slightly the strict filename requirements for post files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full release notes and a migration guide are available &lt;a href="/changelog"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h1 id="in-progress"&gt;
In Progress&lt;/h1&gt;

&lt;p&gt;The changes allowing base URLs to be set from &lt;code&gt;config.yml&lt;/code&gt;, instead of being a flag from the CLI or build plugins,
brings us closer to having &lt;em&gt;relative&lt;/em&gt; urls for generated links instead &lt;em&gt;absolute&lt;/em&gt; as they are now. Relative URLs don't
include a base URL, but instead provide a path from the current page to the linked one, and will make it possible to
view an Orchid site directly in the filesystem instead of needing to start an HTTP server to view it.&lt;/p&gt;

&lt;p&gt;There is still work to be done on this, but it's now considered a mid- to short-term goal for Orchid instead of a
long-term one, and it will be a &lt;em&gt;huge&lt;/em&gt; win for those of you using Orchid for documentation. Thank you to everyone who
has been asking for this, I hope to be able to provide this feature for you soon!&lt;/p&gt;




&lt;p&gt;Are you interested in getting started with Orchid? There simply is no better way to manage all the documentation for
your project, and I'd love to help you get set up!&lt;/p&gt;

&lt;p&gt;If you have an open-source project that needs docs, are building a new portfolio, or are building any other kind of
static site, I want to work with you to get you set up with Orchid! Comment on this post, send me a PM here on Dev.to,
reach out on &lt;a href="https://gitter.im/JavaEden/Orchid" rel="noopener noreferrer"&gt;Gitter&lt;/a&gt;, or &lt;a href="https://www.caseyjbrooks.com/contact/" rel="noopener noreferrer"&gt;contact me here&lt;/a&gt;
and I will be with you every step of the way.&lt;/p&gt;

&lt;p&gt;And as always, let me know if you start using Orchid so I can feature you in the next update!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>orchid</category>
      <category>jamstack</category>
      <category>webdev</category>
    </item>
    <item>
      <title>0.19.0 Released</title>
      <dc:creator>Casey Brooks</dc:creator>
      <pubDate>Sun, 23 Feb 2020 23:37:10 +0000</pubDate>
      <link>https://dev.to/orchidhq/0-19-0-released-1c00</link>
      <guid>https://dev.to/orchidhq/0-19-0-released-1c00</guid>
      <description>&lt;p&gt;The next major version of Orchid, 0.19.0, is now available! Check out the full release notes
&lt;a href="https://orchid.run/changelog" rel="noopener noreferrer"&gt;here&lt;/a&gt;, and stick around for a general update of the development and community around Orchid!&lt;/p&gt;

&lt;p&gt;This is the official Orchid newsletter, the newest and best documentation site generator. There is a growing need to
keep the community up-to-date on all the happenings around Orchid, and here I will share Orchid's progress, milestones,
and future plans! Follow along with this series to stay on top of Orchid's newest features, track adoption on Github,
and see who's using Orchid!&lt;/p&gt;

&lt;h2 id="on-github"&gt;
On Github&lt;/h2&gt;

&lt;p&gt;Orchid is now at 329 stars on Github, and since the last newsletter has received a PR from
&lt;a href="https://github.com/tomb50" rel="noopener noreferrer"&gt;tomb50&lt;/a&gt; to improve Orchid's Asciidoctor integration. And thank you to everyone else who has
been giving Orchid a try, reaching out with questions and feedback, and generally supporting this growing community!&lt;/p&gt;

&lt;h2 id="whats-new"&gt;
What's New?&lt;/h2&gt;

&lt;p&gt;Since the release of 0.18.0, I've been able to work on many smaller "quality-of-life" improvements to Orchid that have
been suggested by the community. Some of these changes required breaking the internal API (thus the major version bump
to 0.19.0), but the last few releases have mainly just been smaller new features and improvements to existing plugins.
Below are the new features available in 0.19.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Improve Asciidoc formatting. Specifically, other files relative to the source file can now be included.&lt;/li&gt;
&lt;li&gt;Automatically add &lt;code&gt;CNAME&lt;/code&gt; files to GithubPages deploys, if not already present in the deployed site. The CNAME value
will be inferred from the base URL of the site during the deploy.&lt;/li&gt;
&lt;li&gt;Adds &lt;code&gt;feedLinks&lt;/code&gt; metaComponent to Posts plugin, which adds &lt;code&gt;&amp;lt;link rel='alternate'&amp;gt;&lt;/code&gt; tags to page heads, pointing to
the generated feed pages.&lt;/li&gt;
&lt;li&gt;Images uploaded and referenced from GitLab wikis are now copied over automatically with the rest of the site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full release notes and a migration guide are available &lt;a href="https://orchid.run/changelog" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h1 id="in-progress"&gt;
In Progress&lt;/h1&gt;

&lt;p&gt;Most work over the past month has been refactoring needed to support a better Asciidoctor experience, and generally
clean up some of the older parts of the Orchid codebase. This is work that will eventually be extracted to a separate
library, so be on the look-out for that!&lt;/p&gt;

&lt;p&gt;There is still work to be done with the Asciidoctor integration, but it's getting better all the time, and work on the
Snippets plugin will be picking back up soon, too. I've already had some good feedback and suggestions for this feature
which I will be taking into account as I build it out, so be sure to leave your thoughts on
&lt;a href="https://github.com/orchidhq/Orchid/issues/293" rel="noopener noreferrer"&gt;the appropriate issue&lt;/a&gt;, to make this plugin exactly what you need!&lt;/p&gt;




&lt;p&gt;Are you interested in getting started with Orchid? There simply is no better way to manage all the documentation for
your project, and I'd love to help you get set up!&lt;/p&gt;

&lt;p&gt;If you have an open-source project that needs docs, are building a new portfolio, or are building any other kind of
static site, I want to work with you to get you set up with Orchid! Comment on this post, send me a PM here on Dev.to,
reach out on &lt;a href="https://gitter.im/JavaEden/Orchid" rel="noopener noreferrer"&gt;Gitter&lt;/a&gt;, or &lt;a href="https://www.caseyjbrooks.com/contact/" rel="noopener noreferrer"&gt;contact me here&lt;/a&gt;
and I will be with you every step of the way.&lt;/p&gt;

&lt;p&gt;And as always, let me know if you start using Orchid so I can feature you in the next update!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>orchid</category>
      <category>jamstack</category>
      <category>webdev</category>
    </item>
    <item>
      <title>January 2020 Orchid Newsletter: Gaining Traction</title>
      <dc:creator>Casey Brooks</dc:creator>
      <pubDate>Thu, 23 Jan 2020 16:08:40 +0000</pubDate>
      <link>https://dev.to/orchidhq/orchid-is-now-on-producthunt-1il3</link>
      <guid>https://dev.to/orchidhq/orchid-is-now-on-producthunt-1il3</guid>
      <description>&lt;p&gt;It's so incredible to see this project grow from a tiny seedling to the maturing sprout it is now. Especially since the
0.18.0 release, I've seen more people try out Orchid and adopt it for their documentation sites, and I'm so incredibly
grateful for each and every download. But this is only the beginning, Orchid still has lots of growing-up to do, so why
don't you come along for the ride and help shape the future of Orchid!&lt;/p&gt;

&lt;p&gt;This is the official Orchid newsletter, the newest and best documentation site generator. There is a growing need to
keep the community up-to-date on all the happenings around Orchid, and here I will share Orchid's progress, milestones,
and future plans! Follow along with this series to stay on top of Orchid's newest features, track adoption on Github,
and see who's using Orchid!&lt;/p&gt;

&lt;h2 id="on-github"&gt;
On Github&lt;/h2&gt;

&lt;p&gt;Orchid has been growing so much since December's 0.18.0 release! Its now at 313 stars and version 0.18.2, with more
downloads, new sites, and issues created than ever before! And it's all thanks to this wonderful community of
individuals believing in a better way to do documentation, so thank you all!&lt;/p&gt;

&lt;p&gt;The holidays were fairly quiet in terms of new releases and contributions, but &lt;a href="https://github.com/tomb50" rel="noopener noreferrer"&gt;Tom Beadman&lt;/a&gt;
helped immensely with improving the asciidoctor integration. A small PR with huge impact for improving the
quality-of-life for Asciidoctor fans, which now includes support for &lt;code&gt;include::[]&lt;/code&gt; macros!&lt;/p&gt;

&lt;p&gt;I also merged work &lt;a href="https://github.com/DanySK" rel="noopener noreferrer"&gt;Danilo Pianini&lt;/a&gt; had done for improving the YouTube tag to support
aspect-ratios instead of fixed sized.&lt;/p&gt;

&lt;h2 id="whats-new"&gt;
What's New?&lt;/h2&gt;

&lt;p&gt;Now that 0.18.0 is released, I've been able to work on some smaller features I've been wanting to support for a while. I
had to restrain myself from working on them for 0.18.0 to prevent feature-creep, but it's been nice finally getting to
add the following features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://orchid.run/plugins/orchidbible" rel="noopener noreferrer"&gt;OrchidBible&lt;/a&gt; plugin has been broken for some time, as the APIs driving it had been decommissioned.
I have removed the previously-broken &lt;code&gt;bible()&lt;/code&gt; function, which pre-rendered Bible verses, and replaced it with a new
&lt;a href="https://faithlife.com/products/reftagger" rel="noopener noreferrer"&gt;Faithlife Reftagger&lt;/a&gt; meta-component, which will automatically create
popups for all Bible verses it finds on the page!&lt;/li&gt;
&lt;li&gt;Support for &lt;a href="https://mermaid-js.github.io/mermaid" rel="noopener noreferrer"&gt;Mermaid JS&lt;/a&gt; markup has been added to the
&lt;a href="https://orchid.run/plugins/orchiddiagrams" rel="noopener noreferrer"&gt;OrchidDiagrams&lt;/a&gt; plugin. As Mermaid is a javascript library, support is added through a meta-component
instead of using pre-rendered markup like PlantUML.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;youtube&lt;/code&gt; template tag in &lt;a href="https://orchid.run/plugins/orchidwritersblocks" rel="noopener noreferrer"&gt;OrchidWritersBlocks&lt;/a&gt; can now display videos of a given aspect-ratio,
making them better-suited for responsive designs.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://orchid.run/plugins/orchidchangelog" rel="noopener noreferrer"&gt;OrchidChangelog&lt;/a&gt; now supports single-file changelogs, such as the
&lt;a href="https://keepachangelog.com/en/1.0.0/" rel="noopener noreferrer"&gt;Keep A Changelog format&lt;/a&gt; format.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id="in-progress"&gt;
In Progress&lt;/h1&gt;

&lt;p&gt;Supporting include macros for Asciidoctor was a huge step forward, but there's still work to be done to improve that
integration, and you can expect more Asciidoctor improvements in the coming weeks, with &lt;code&gt;image::[]&lt;/code&gt; being the next
target.&lt;/p&gt;

&lt;p&gt;In addition to a few more quality-of-life improvements planned for the near-future, I've begun work on a new
&lt;code&gt;OrchidSnippets&lt;/code&gt; plugin! More details will come in a future post, but I'd love to get your feedback and suggestions for
its implementation and usage in &lt;a href="https://github.com/orchidhq/Orchid/issues/293" rel="noopener noreferrer"&gt;this issue&lt;/a&gt; on GitHub!&lt;/p&gt;





&lt;p&gt;Are you interested in getting started with Orchid? There simply is no better way to manage all the documentation for
your project, and I'd love to help you get set up!&lt;/p&gt;

&lt;p&gt;If you have an open-source project that needs docs, are building a new portfolio, or are building any other kind of
static site, I want to work with you to get you set up with Orchid! Comment on this post, send me a PM here on Dev.to,
reach out on &lt;a href="https://gitter.im/JavaEden/Orchid" rel="noopener noreferrer"&gt;Gitter&lt;/a&gt;, or &lt;a href="https://www.caseyjbrooks.com/contact/" rel="noopener noreferrer"&gt;contact me here&lt;/a&gt;
and I will be with you every step of the way.&lt;/p&gt;

&lt;p&gt;And as always, let me know if you start using Orchid so I can feature you in the next update!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>orchid</category>
      <category>jamstack</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Make a Personal Blog with Orchid</title>
      <dc:creator>Casey Brooks</dc:creator>
      <pubDate>Thu, 12 Dec 2019 16:53:26 +0000</pubDate>
      <link>https://dev.to/orchidhq/make-a-personal-blog-with-orchid-24ji</link>
      <guid>https://dev.to/orchidhq/make-a-personal-blog-with-orchid-24ji</guid>
      <description>&lt;p&gt;&lt;a href="https://orchid.run" rel="noopener noreferrer"&gt;Orchid&lt;/a&gt; was created to create amazing project documentation sites, but it is by no means limited to documentation. Orchid is equally good at producing blogs for your portfolio site, or even for adding a newsletter to your docs to further&lt;br&gt;
engage with your users!&lt;/p&gt;

&lt;p&gt;This tutorial will walk you through how to create a blog, complete with archives for all your posts, and get it deployed
to Netlify. If you want to jump right into a working project, you can find everything described here in the OrchidTutorials example project.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/orchidhq" rel="noopener noreferrer"&gt;
        orchidhq
      &lt;/a&gt; / &lt;a href="https://github.com/orchidhq/OrchidTutorials" rel="noopener noreferrer"&gt;
        OrchidTutorials
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A collection of tutorial projects to help you learn how to use Orchid
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Orchid Tutorials&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;This repo contains the projects used in the official Orchid tutorials. Each project top-level directory here is its own
project, and can be run by navigating into that project and running the appropriate commands as described in the Orchid
tutorials.&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/orchidhq/OrchidTutorials" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;Alternatively, you can simply click the "Deploy to Netlify" button below to automatically clone, build, and deploy the
OrchidStarter repo to the Netlify CDN. Most of what is in this tutorial is also included in that site, so you can
follow along with this tutorial using the &lt;a href="https://github.com/JavaEden/OrchidStarter" rel="noopener noreferrer"&gt;starter repo&lt;/a&gt; as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.netlify.com/start/deploy?repository=https://github.com/orchidhq/OrchidStarter" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.netlify.com%2Fimg%2Fdeploy%2Fbutton.svg" alt="Deploy to Netlify" width="179" height="32"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This tutorial was originally posted on &lt;a href="https://orchid.run" rel="noopener noreferrer"&gt;https://orchid.run&lt;/a&gt;. Visit the &lt;a href="https://orchid.run/wiki/learn/tutorials/how-to-blog" rel="noopener noreferrer"&gt;original tutorial&lt;/a&gt; for the most up-to-date version.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id="getting-started"&gt;
Getting Started&lt;/h2&gt;

&lt;p&gt;We'll be using Gradle for this project, and Orchid runs as a Gradle plugin. So let's get our &lt;code&gt;settings.gradle&lt;/code&gt; and we'll
also set up the &lt;code&gt;build.gradle&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="token comment"&gt;// settings.gradle&lt;/span&gt;
rootProject&lt;span class="token operator"&gt;.&lt;/span&gt;name &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string"&gt;'My Awesome Blog'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="token comment"&gt;// build.gradle&lt;/span&gt;
&lt;span class="token comment"&gt;// 1. Apply Orchid plugin&lt;/span&gt;
plugins &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    id &lt;span class="token string gstring"&gt;"com.eden.orchidPlugin"&lt;/span&gt; version &lt;span class="token string gstring"&gt;"0.18.0"&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;

&lt;span class="token comment"&gt;// 2. Include Orchid dependencies&lt;/span&gt;
dependencies &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    orchidCompile &lt;span class="token string gstring"&gt;"io.github.javaeden.orchid:OrchidBlog:0.18.0"&lt;/span&gt;
    orchidCompile &lt;span class="token string gstring"&gt;"io.github.javaeden.orchid:OrchidFutureImperfect:0.18.0"&lt;/span&gt;
    orchidCompile &lt;span class="token string gstring"&gt;"io.github.javaeden.orchid:OrchidSearch:0.18.0"&lt;/span&gt;
    orchidCompile &lt;span class="token string gstring"&gt;"io.github.javaeden.orchid:OrchidPluginDocs:0.18.0"&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;

&lt;span class="token comment"&gt;// 3. Get dependencies from JCenter&lt;/span&gt;
repositories &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    &lt;span class="token function"&gt;jcenter&lt;/span&gt;&lt;span class="token punctuation"&gt;(&lt;/span&gt;&lt;span class="token punctuation"&gt;)&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;

&lt;span class="token comment"&gt;// 4. Use the 'FutureImperfect' theme, and set the URL it will have on Github Pages&lt;/span&gt;
orchid &lt;span class="token punctuation"&gt;{&lt;/span&gt;
    theme &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string gstring"&gt;"FutureImperfect"&lt;/span&gt;
    baseUrl &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string gstring"&gt;"https://project.netlify.com"&lt;/span&gt;
    version &lt;span class="token operator"&gt;=&lt;/span&gt; &lt;span class="token string gstring"&gt;"1.0.0"&lt;/span&gt;
&lt;span class="token punctuation"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is all that's required to run your Orchid site! There are still a few things we need to do to set up the blog, but
you can run Orchid right now with &lt;code&gt;./gradlew orchidServe&lt;/code&gt; and view the site on http://localhost:8080. It should give you
an output like the following:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;./gradlew :docs:orchidServe

&amp;gt; Task :docs:orchidServe
Using the following modules: 
--------------------
 * com.eden.orchid.StandardModule

Auto-loaded modules: 
--------------------
 * com.eden.orchid.editorial.EditorialModule
 * com.eden.orchid.impl.compilers.markdown.FlexmarkModule
 * com.eden.orchid.impl.compilers.pebble.PebbleModule
 * com.eden.orchid.kotlindoc.KotlindocModule
 * com.eden.orchid.pages.PagesModule
 * com.eden.orchid.search.SearchModule
 * com.eden.orchid.wiki.WikiModule

Flag values: 
--------------------
-adminTheme: Default
-baseUrl: https://project.netlify.com
-defaultTemplateExtension: peb
-dest: ...
-dryDeploy: false
-environment: debug
-logLevel: VERBOSE
-port: 8080
-src: ...
-task: serve
-theme: Editorial
-version: 1.0.0

[INFO] Orchid: Running Orchid version 0.16.0, site version unspecified in debug environment
[INFO] OrchidWebserver: Webserver Running at http://localhost:8080
[INFO] OrchidWebsocket: Websocket running at http://localhost:8081/
[INFO] TaskServiceImpl: Build Starting...
[INFO] GeneratorServiceImpl: Indexing [10000: assets]
[INFO] GeneratorServiceImpl: Indexing [1000: home]
[INFO] GeneratorServiceImpl: Indexing [1000: kotlindoc]
[INFO] GeneratorServiceImpl: Indexing [1000: pages]
[INFO] GeneratorServiceImpl: Indexing [1000: wiki]
[INFO] GeneratorServiceImpl: Indexing [11: sitemap]
[INFO] GeneratorServiceImpl: Indexing [10: indices]
[INFO] GeneratorServiceImpl: Generating [10000: assets]
[INFO] GeneratorServiceImpl: Generating [1000: home]
[INFO] GeneratorServiceImpl: Generating [1000: kotlindoc]
[INFO] GeneratorServiceImpl: Generating [1000: pages]
[INFO] GeneratorServiceImpl: Generating [1000: wiki]
[INFO] GeneratorServiceImpl: Generating [11: sitemap]
[INFO] GeneratorServiceImpl: Generating [10: indices]

Build Metrics:
┌───────┬────────────┬───────────────┬─────────────────┬───────────────────────────┬─────────────────────────────┐
│       │ Page Count │ Indexing Time │ Generation Time │ Mean Page Generation Time │ Median Page Generation Time │
├───────┼────────────┼───────────────┼─────────────────┼───────────────────────────┼─────────────────────────────┤
│  home │     1      │     54ms      │      481ms      │           472ms           │            472ms            │
├───────┼────────────┼───────────────┼─────────────────┼───────────────────────────┼─────────────────────────────┤
│ TOTAL │          1 │      3s 496ms │           520ms │                     472ms │                       472ms │
└───────┴────────────┴───────────────┴─────────────────┴───────────────────────────┴─────────────────────────────┘

Build Complete
Generated 1 page in 4s 18ms


Webserver Running at http://localhost:8080
Hit [CTRL-C] to stop the server and quit Orchid
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You will also see the basic site served on localhost:8080, which looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-01.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-01.png" alt="empty Orchid site" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But let's move on to the next step: adding content to the site!&lt;/p&gt;

&lt;h2 id="adding-content"&gt;
Adding Content&lt;/h2&gt;

&lt;h3 id="homepage"&gt;
Homepage&lt;/h3&gt;

&lt;p&gt;The first thing anyone will see when landing on your site is your Homepage. Orchid creates this page based on a
&lt;code&gt;homepage.md&lt;/code&gt; file in the root of your site's &lt;em&gt;resources&lt;/em&gt;, which are located by default in &lt;code&gt;src/orchid/resources&lt;/code&gt;. Let's
start by creating this file and adding a short description of our project to it.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// docs/src/orchid/resources/homepage.md
&lt;span class="token title important"&gt;&lt;span&gt;#&lt;/span&gt; My Blog&lt;/span&gt;

This is a short description of this blog.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-02.png" alt="Orchid Site with Homepage content" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A common thing to add to your blog homepage is a list of your latest blog posts. But before we add that, we'll need to
write some blog posts first.&lt;/p&gt;

&lt;h3 id="blog-posts"&gt;
Blog Posts&lt;/h3&gt;

&lt;p&gt;Blog posts are Markdown files in your &lt;code&gt;posts/&lt;/code&gt; directory, where each file is a separate blog post. The filename of the
post must be in the format of &lt;code&gt;YYYY-MM-DD-[post-title].md&lt;/code&gt;, with the publication date and the "slug" of the post which
will be its path in its URL.&lt;/p&gt;

&lt;p&gt;So let's add a few posts, such as &lt;code&gt;posts/2019-01-01-post-one.md&lt;/code&gt;, &lt;code&gt;posts/2019-02-01-post-two.md&lt;/code&gt;, and
&lt;code&gt;posts/2019-03-01-post-three.md&lt;/code&gt;. The contents of each file should start with a Front Matter section, where you can
specify the post's title, its tags, and other metadata, and the actual post content after that. Front Matter is a block
of YAML between pairs of triple-dashed lines.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="token title important"&gt;# posts/2019-01-01-post-one.md
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
title: Example Post 1
featuredImage: assets/media/pic01.jpg
tags:
    &lt;span class="token list punctuation"&gt;-&lt;/span&gt; one
    &lt;span class="token title important"&gt;- two
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;

Example Post 1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You'll now have a blog post at http://localhost:8080/2019/1/1/post-one that looks like the following;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-03.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-03.png" alt="Orchid blog post" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that we have some blog posts set up, we can go back and add the latest posts to our homepage. We can do by adding
a &lt;strong&gt;component&lt;/strong&gt; to the homepage's Front Matter, configured to display the latest posts. Orchid's Components are just a
list of "blocks" which are rendered to the page in order. There are many different types of components, and different
plugins can add their own. An example is the &lt;code&gt;recentPosts&lt;/code&gt; component from the OrchidPosts plugin. We can also add the
&lt;code&gt;pageContent&lt;/code&gt; component, which adds the Markdown content of the &lt;code&gt;homepage.md&lt;/code&gt;. If you don't define any components this
one is added automatically, but if you use any additional components you'll have to add it yourself.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="token title important"&gt;# homepage.md
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
components:
  &lt;span class="token list punctuation"&gt;-&lt;/span&gt; type: pageContent
  &lt;span class="token list punctuation"&gt;-&lt;/span&gt; type: recentPosts
    limit: 4
    noWrapper: true
    template:
      &lt;span class="token title important"&gt;- 'includes/postPreview_large'
&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class="token title important"&gt;&lt;span&gt;#&lt;/span&gt; My Blog&lt;/span&gt;

This is a short description of this blog.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-04.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-04.png" alt="Orchid blog post" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3 id="creating-archives"&gt;
Creating Archives&lt;/h3&gt;

&lt;h4 id="post-archive"&gt;
Post Archive&lt;/h4&gt;

&lt;p&gt;Now, while it is nice to show the most recent posts on your site's homepage, if you've got more than a couple posts you
simply can't show a complete archive on the homepage. Instead, we can use the OrchidTaxonomies plugin to generate proper
archives.&lt;/p&gt;

&lt;p&gt;Orchid is designed around a concept of "collections" of pages. When we set up the blog posts, Orchid also created a
&lt;code&gt;posts&lt;/code&gt; collection. We can tell the Taxonomies plugin to generate an archive of any of our collections from any plugin,
making it simple to create any archives we may need.&lt;/p&gt;

&lt;p&gt;Configuring archives is done through a &lt;code&gt;config.yml&lt;/code&gt; file in our site resources. This is the main entry-point to
configuring &lt;em&gt;anything&lt;/em&gt; in our site. The following snippet will set up a &lt;em&gt;collection archive&lt;/em&gt; for all our blog posts.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="token comment"&gt;# docs/src/orchid/resources/config.yml&lt;/span&gt;
&lt;span class="token key atrule"&gt;taxonomies&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
  &lt;span class="token key atrule"&gt;collectionArchives&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
    &lt;span class="token punctuation"&gt;-&lt;/span&gt; &lt;span class="token key atrule"&gt;collectionType&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;'posts'&lt;/span&gt;
      &lt;span class="token key atrule"&gt;collectionId&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;'blog'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now, if we visit http://localhost:8080/blog, we will see a listing of all our blog posts. As our blog grows, this
archive will automatically become paginated, keeping any single page from growing too large.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-05.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-05.png" alt="Collection Archive" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You may wish to create archives for other collections as well. If you visit http://localhost:8080/admin while your site
is serving locally, you can view Orchid's admin panel where we can see a list of all the collections that have been set
up for us.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-06.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-06.png" alt="Orchid Admin Panel Collections" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4 id="tag-archives"&gt;
Tag Archives&lt;/h4&gt;

&lt;p&gt;But the full listing of posts may not be the only kind of archives we want! Most blogs will also contain listings of
posts by tag (remember, we provided a list of tags to our posts earlier!). Orchid did not create a collection for these
tagged pages, but that doesn't mean we can't create an archive for them too! The Taxonomies plugin is able to generate
&lt;em&gt;any&lt;/em&gt; kind of arbitrary archive, based on the metadata attached to each post. These are called &lt;em&gt;taxonomy archives&lt;/em&gt;,
because you get to create any kind of taxonomy (or labelling) you need.&lt;/p&gt;

&lt;p&gt;Configuration of a taxonomy archive is similar to a collection archive, but instead of giving it the values for a
collection, we tell it a property to look for in our posts' Front Matter, such as &lt;code&gt;tags&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="token key atrule"&gt;taxonomies&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
  &lt;span class="token key atrule"&gt;collectionArchives&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
    &lt;span class="token punctuation"&gt;-&lt;/span&gt; &lt;span class="token key atrule"&gt;collectionType&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;'posts'&lt;/span&gt;
      &lt;span class="token key atrule"&gt;collectionId&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;'blog'&lt;/span&gt;
  &lt;span class="token key atrule"&gt;taxonomies&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
    &lt;span class="token punctuation"&gt;-&lt;/span&gt; &lt;span class="token key atrule"&gt;key&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; tags
      &lt;span class="token key atrule"&gt;single&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token boolean important"&gt;false&lt;/span&gt;
      &lt;span class="token key atrule"&gt;orderBy&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
        &lt;span class="token punctuation"&gt;-&lt;/span&gt; entryCount
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This will now go through all our posts, find any that have a &lt;code&gt;tags&lt;/code&gt; property in its Front Matter, and add it to that
archive. And this will actually create &lt;em&gt;two&lt;/em&gt; archives for each "taxonomy": one listing all the pages with each tag (a
term archive), and another archive simply listing all the tags that it found (a taxonomy archive).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;http://localhost:8080/tags&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-07.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-07.png" alt="Taxonomy Archive" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;http://localhost:8080/tags/one&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-08.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-08.png" alt="Term Archive" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3 id="site-infotheme-configuration"&gt;
Site Info/Theme Configuration&lt;/h3&gt;

&lt;p&gt;With the skeleton of our site content set up, it's time to make some customizations to the theme and add additional info
about your site, such as its name and the author of your posts. These can be added to &lt;code&gt;config.yml&lt;/code&gt;, just like how we
configured the archives.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="token comment"&gt;# docs/src/orchid/resources/config.yml&lt;/span&gt;
&lt;span class="token key atrule"&gt;site&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
  &lt;span class="token key atrule"&gt;about&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
    &lt;span class="token key atrule"&gt;siteName&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; My Blog
    &lt;span class="token key atrule"&gt;siteDescription&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; This is a short description of this blog.
    &lt;span class="token key atrule"&gt;avatar&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;'http://lorempixel.com/320/320/city/'&lt;/span&gt;
&lt;span class="token key atrule"&gt;theme&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
  &lt;span class="token key atrule"&gt;social&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
    &lt;span class="token key atrule"&gt;github&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;'username/project'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-09.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-09.png" alt="Orchid site with some configuration" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But these configuration values didn't just come out of nowhere. Going back to the admin panel, you can find all the
options available for customization for your theme, for components, and for just about anything else.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-10.png" alt="Orchid site with some configuration" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's a final bit of info that we should need to &lt;code&gt;config.yml&lt;/code&gt; before finishing: setting the author for all our posts.&lt;/p&gt;

&lt;p&gt;You would normally add the necessary configuration to each post's Front Matter, but it is really difficult,
time-consuming, and error-prone to copy this data to each post file. Instead, Orchid has &lt;em&gt;Archetypes&lt;/em&gt; which allow you to
set that configuration once in &lt;code&gt;config.yml&lt;/code&gt; and have it shared amongst a bunch of pages just as if it were added to the
Front Matter of each one.&lt;/p&gt;

&lt;p&gt;For adding configuration values to all post pages, use &lt;code&gt;posts.postPages&lt;/code&gt; in the &lt;code&gt;config.yml&lt;/code&gt;. We'll add an author to the
blog by adding an item to the &lt;code&gt;posts.authors&lt;/code&gt; list, and using the archetype we'll set the author for each page to the
one you just set up:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="token comment"&gt;# docs/src/orchid/resources/config.yml&lt;/span&gt;
&lt;span class="token key atrule"&gt;posts&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
  &lt;span class="token key atrule"&gt;authors&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
    &lt;span class="token punctuation"&gt;-&lt;/span&gt; &lt;span class="token key atrule"&gt;name&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;'Author One'&lt;/span&gt;
      &lt;span class="token key atrule"&gt;avatar&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;'http://lorempixel.com/320/320/animals/'&lt;/span&gt;
      &lt;span class="token key atrule"&gt;email&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;'email@domain.tld'&lt;/span&gt;
  &lt;span class="token key atrule"&gt;postPages&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt;
    &lt;span class="token key atrule"&gt;author&lt;/span&gt;&lt;span class="token punctuation"&gt;:&lt;/span&gt; &lt;span class="token string"&gt;'Author One'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-11.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Forchid.run%2Fwiki%2Flearn%2Ftutorials%2Fmedia%2Fblog-11.png" alt="Blog post with tag archive links" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id="deploy-on-netlify"&gt;
Deploy On Netlify&lt;/h2&gt;

&lt;p&gt;Our site is now ready to be deployed! For blogs and personal sites, you really can't go wrong with Netlify as your site
host. It offers everything you could want for both small and large sites, such as purchasing custom domains, form
handling, and automated site deploys. While Orchid has its own Netlify publisher for advanced use cases, it's far
simpler to use Netlify as intended.&lt;/p&gt;

&lt;p&gt;All you need to do is create an account at https://www.netlify.com/, add a &lt;code&gt;netlify.toml&lt;/code&gt; file to the root of your repo
with the following content, and push to GitHub.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;[build]
  base    = ""
  publish = "build/docs/orchid"
  command = "./gradlew orchidBuild -Penv=prod"
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now, once you've connected that repo to Netlify, they will take care of the rest! Of course, this is just the minimum
needed to deploy, and I'd encourage you to also check out their &lt;a href="https://docs.netlify.com/#get-started" rel="noopener noreferrer"&gt;full documentation&lt;/a&gt;
for more help building and deploying your Orchid site on Netlify.&lt;/p&gt;

&lt;h2 id="conclusion"&gt;
Conclusion&lt;/h2&gt;

&lt;p&gt;And with all that, our blog site is finished! Now it may have seemed like a ton of work getting all that setup, but
let's recall all the features included in this site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A homepage that displays our latest blog posts&lt;/li&gt;
&lt;li&gt;A full, paginated archive of all blog posts&lt;/li&gt;
&lt;li&gt;A listing of all tags in your blog&lt;/li&gt;
&lt;li&gt;An archive for all the pages with each tag&lt;/li&gt;
&lt;li&gt;Ability to set the author for each post&lt;/li&gt;
&lt;li&gt;Ability to easily change the configurations for all your blog posts from a single location, instead of copying data
to each post&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And everything outlined in this tutorial is really just a sampling of the full functionality available in Orchid! Orchid
has other plugins for different source code documentation, presentations, wikis, and much more. Check out the full list
of plugins &lt;a href="https://orchid.run/plugins" rel="noopener noreferrer"&gt;here&lt;/a&gt;, or you can even make your own!&lt;/p&gt;

&lt;p&gt;Thanks for following along, happy blogging!&lt;/p&gt;

</description>
      <category>orchid</category>
      <category>blog</category>
      <category>static</category>
    </item>
    <item>
      <title>Orchid 0.18.0, The New Face of Orchid</title>
      <dc:creator>Casey Brooks</dc:creator>
      <pubDate>Tue, 10 Dec 2019 16:12:46 +0000</pubDate>
      <link>https://dev.to/orchidhq/orchid-0-18-0-the-new-face-of-orchid-4lnd</link>
      <guid>https://dev.to/orchidhq/orchid-0-18-0-the-new-face-of-orchid-4lnd</guid>
      <description>&lt;p&gt;It's. Finally. Here. I've been working on and teasing a new major version of Orchid for several months, and it's finally
available! This release represents a major step in the maturity of Orchid, coming alongside the move to a new GitHub
organization, a completely redesigned website, and a brand new logo!&lt;/p&gt;

&lt;p&gt;This is the official Orchid newsletter, the newest and best documentation site generator. There is a growing need to
keep the community up-to-date on all the happenings around Orchid, and here I will share Orchid's progress, milestones,
and future plans! Follow along with this series to stay on top of Orchid's newest features, track adoption on Github,
and see who's using Orchid!&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/orchidhq" rel="noopener noreferrer"&gt;
        orchidhq
      &lt;/a&gt; / &lt;a href="https://github.com/orchidhq/Orchid" rel="noopener noreferrer"&gt;
        Orchid
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Build and deploy beautiful documentation sites that grow with you
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/" rel="nofollow noopener noreferrer"&gt;
    &lt;img width="460" src="https://camo.githubusercontent.com/bb0153811a1ae7ec58b4836825a32c2982f1f6822f0fbe8bd3cb2e7a7b038eeb/68747470733a2f2f6f726368696468712e6769746875622e696f2f4f72636869642f6173736574732f7376672f6f72636869642f6c6f676f5f746f705f6c696768742e737667" title="Orchid" alt="Orchid"&gt;
  &lt;/a&gt;
  &lt;br&gt;
  &lt;strong&gt;Build and deploy beautiful documentation sites that grow with you&lt;/strong&gt;
&lt;/p&gt;




&lt;p&gt;
  &lt;a href="https://search.maven.org/artifact/io.github.copper-leaf.orchid/orchid-core" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="Maven Central (Releases)" src="https://camo.githubusercontent.com/a65a457ca8dbc4c986ffe4c6c22f1d3520afc40f9d1f9a04d881a32a205bb15f/68747470733a2f2f696d672e736869656c64732e696f2f6d6176656e2d63656e7472616c2f762f696f2e6769746875622e636f707065722d6c6561662e6f72636869642f6f72636869642d636f72653f6c6162656c3d52656c65617365"&gt;
  &lt;/a&gt;
  &lt;a href="https://s01.oss.sonatype.org/content/repositories/snapshots/io/github/copper-leaf/orchid/orchid-core" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="Sonatype Nexus (Snapshots)" src="https://camo.githubusercontent.com/b6adbc1d22e7babe344faa6e9ea3792a04c54bc9ef5dea80a105e1cc065d38d0/68747470733a2f2f696d672e736869656c64732e696f2f6e657875732f732f696f2e6769746875622e636f707065722d6c6561662e6f72636869642f6f72636869642d636f72653f6c6162656c3d536e617073686f74267365727665723d68747470732533412532462532467330312e6f73732e736f6e61747970652e6f7267"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/orchidhq/Orchid/actions/workflows/push_dev.yml" rel="noopener noreferrer"&gt;
    &lt;img src="https://github.com/orchidhq/Orchid/actions/workflows/push_dev.yml/badge.svg?branch=dev" title="Build Status" alt="Build Status"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/orchidhq/Orchid/License.md" rel="noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/1b0c7e4911720d0444c16a1ffd145a039f14a1a7305362ab51184f757a4dd6bc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d47504c25323076332d626c75652e737667" title="License: GPL-3.0" alt="License: GPL-3.0"&gt;
  &lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/c27002240e14f6ffa0e6f0bf6f172016e14e366360a02cd5d7193d37415feee1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4a444b2d382d2d31362d7265643f7374796c653d666c6174266c6f676f3d6a617661"&gt;&lt;img src="https://camo.githubusercontent.com/c27002240e14f6ffa0e6f0bf6f172016e14e366360a02cd5d7193d37415feee1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4a444b2d382d2d31362d7265643f7374796c653d666c6174266c6f676f3d6a617661" title="JDK: 8-16" alt="JDK: 8-16"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart" rel="nofollow noopener noreferrer"&gt;Quick-Start&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/wiki/learn" rel="nofollow noopener noreferrer"&gt;Tutorials&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/showcase" rel="nofollow noopener noreferrer"&gt;Showcase&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://gitter.im/JavaEden/Orchid" rel="nofollow noopener noreferrer"&gt;Support&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick-Start&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart#gradle" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Forchidhq%2FOrchid%2FHEAD%2Fdocs%2Fsrc%2Forchid%2Fresources%2Fassets%2Fsvg%2Fgradle.svg" title="Gradle" alt="Gradle" width="200" height="50"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart#maven" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Forchidhq%2FOrchid%2FHEAD%2Fdocs%2Fsrc%2Forchid%2Fresources%2Fassets%2Fsvg%2Fmaven.svg" title="Maven" alt="Maven" width="200" height="50"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart#sbt" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Forchidhq%2FOrchid%2FHEAD%2Fdocs%2Fsrc%2Forchid%2Fresources%2Fassets%2Fsvg%2Fsbt.svg" title="SBT" alt="SBT" width="200" height="50"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;a href="https://app.netlify.com/start/deploy?repository=https://github.com/orchidhq/OrchidStarter" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://camo.githubusercontent.com/de47084e2d5a8948486a2238f1517e465560ec293c4a2cba80020472f8d5c80a/68747470733a2f2f7777772e6e65746c6966792e636f6d2f696d672f6465706c6f792f627574746f6e2e737667" title="Deploy to Netlify" alt="Deploy to Netlify" width="200" height="50" class="js-gh-image-fallback"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;p&gt;Orchid 0.X.X versions are published to &lt;a href="https://jcenter.bintray.com/" rel="nofollow noopener noreferrer"&gt;JCenter&lt;/a&gt; at artifact coordinates like &lt;code&gt;io.github.javaeden.orchid:OrchidCore:0.21.1&lt;/code&gt; or &lt;code&gt;io.github.javaeden.orchid:OrchidWiki:0.21.1&lt;/code&gt;. JCenter is deprecated, and once Orchid 1.0.0 is published, so will all 0.X.X versions.&lt;/p&gt;

&lt;p&gt;Starting with version 1.0.0, Orchid will be published to &lt;a href="https://repo1.maven.org/maven2/" rel="nofollow noopener noreferrer"&gt;MavenCentral&lt;/a&gt; under new artifact coordinates, like &lt;code&gt;io.github.copper-leaf.orchid:orchid-core:1.0.0&lt;/code&gt; or &lt;code&gt;io.github.copper-leaf.orchid:orchid-wiki-feature:1.0.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In-development snapshot versions will be available in &lt;a href="https://s01.oss.sonatype.org/content/repositories/snapshots/" rel="nofollow noopener noreferrer"&gt;Sonatype's new (s01) snapshots repository&lt;/a&gt;. Snapshots are published after every successful build on the &lt;code&gt;dev&lt;/code&gt; branch.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Documentation&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Orchid's User Manual will walk you through the main features of Orchid and give you a deeper understanding of each topic
and feature.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tutorials&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;There are several tutorials designed to walk you through building an Orchid site from scratch. The source for all
tutorials can also be found in the &lt;a href="https://github.com/orchidhq/OrchidTutorials" rel="noopener noreferrer"&gt;OrchidTutorials repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://orchidhq.github.io/Orchid/wiki/learn" rel="nofollow noopener noreferrer"&gt;Tutorials&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Showcase&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;View…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/orchidhq/Orchid" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2 id="on-github"&gt;
On Github&lt;/h2&gt;

&lt;p&gt;As of the time of writing, Orchid is at 278 stars on Github, thank you so much for all the support!&lt;/p&gt;

&lt;p&gt;Since the last update, Orchid has had several contributions, especially during Hacktoberfest, so a special thanks goes
out to the following individuals for their help improving this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/spind42" rel="noopener noreferrer"&gt;spind42&lt;/a&gt; - Fixed an issue running Orchid from Maven&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/jmfayard" rel="noopener noreferrer"&gt;Jean-Michel Fayard&lt;/a&gt; - Added a readme badge for CodeTriage&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/singularsyntax" rel="noopener noreferrer"&gt;Steve S&lt;/a&gt; - Added a new Spotify tag to the &lt;a href="https://orchid.run/plugins/orchidwritersblocks" rel="noopener noreferrer"&gt;OrchidWritersBlocks&lt;/a&gt; plugin&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swaldman" rel="noopener noreferrer"&gt;Steve Waldman&lt;/a&gt; - Wrote a plugin to run Orchid from SBT!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id="whats-new"&gt;
What's New?&lt;/h2&gt;

&lt;p&gt;The long-awaited version &lt;a href="https://github.com/orchidhq/orchid/releases/tag/0.18.0" rel="noopener noreferrer"&gt;0.18.0&lt;/a&gt; has finally arrived!&lt;/p&gt;

&lt;p&gt;This release includes a major cleanup to Orchid's internal APIs, which will help make better plugins moving forward, but
most importantly introduces the new, completely rewritten code documentation system based on
&lt;a href="https://github.com/copper-leaf/kodiak" rel="noopener noreferrer"&gt;Kodiak&lt;/a&gt;. The new system improves consistency of generated docs among all
languages and enables multi-module documentation. It also paves the way to make it significantly easier to develop
integrations for other languages, so let me know which languages you would like to see supported by Orchid!&lt;/p&gt;

&lt;p&gt;Be sure to check out the &lt;a href="https://orchid.run/changelog" rel="noopener noreferrer"&gt;Changelog&lt;/a&gt; for the full list of changes, and the
&lt;a href="https://orchid.run/migration/0_18" rel="noopener noreferrer"&gt;Migration Guide&lt;/a&gt; for help updating to this new version.&lt;/p&gt;

&lt;p&gt;In addition, some of you may have noticed a few changes around the Orchid ecosystem! The Orchid repo has been moved to
@orchidhq on Github, and we have a completely redesigned documentation site and new logos. Together with the 0.18.0
release, this marks the biggest change in the development and ecosystem of Orchid to help cement it is as the
single-best tool for producing documentation websites.&lt;/p&gt;

&lt;p&gt;You can help spread the word about this major milestone by sharing &lt;a href="https://orchid.run" rel="noopener noreferrer"&gt;https://orchid.run&lt;/a&gt; and tagging
@OrchidSSG on Twitter!&lt;/p&gt;

&lt;h2 id="whos-using-orchid"&gt;
Who's Using Orchid?&lt;/h2&gt;

&lt;p&gt;I periodically search GitHub to find new projects getting set up with Orchid. Here are a couple great examples of
projects being documented with Orchid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://alchemistsimulator.github.io/" rel="noopener noreferrer"&gt;The Alchemist Simulator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nhaarman.github.io/acorn/" rel="noopener noreferrer"&gt;Acorn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://camunda.github.io/camunda-rest-client-spring-boot/" rel="noopener noreferrer"&gt;Camunda&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For more examples of sites using Orchid, head on over to our new &lt;a href="https://orchid.run/showcase" rel="noopener noreferrer"&gt;Showcase&lt;/a&gt;, and feel free to reach out
or submit a PR to add your site to the showcase as well!&lt;/p&gt;





&lt;p&gt;Are you interested in getting started with Orchid? There simply is no better way to manage all the documentation for
your project, and I'd love to help you get set up!&lt;/p&gt;

&lt;p&gt;If you have an open-source project that needs docs, are building a new portfolio, or are building any other kind of
static site, I want to work with you to get you set up with Orchid! Comment on this post, send me a PM here on Dev.to,
reach out on &lt;a href="https://gitter.im/JavaEden/Orchid" rel="noopener noreferrer"&gt;Gitter&lt;/a&gt;, or &lt;a href="https://www.caseyjbrooks.com/contact/" rel="noopener noreferrer"&gt;contact me here&lt;/a&gt;
and I will be with you every step of the way.&lt;/p&gt;

&lt;p&gt;And as always, let me know if you start using Orchid so I can feature you in the next update!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>orchid</category>
      <category>static</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Orchid Newsletter: Summer Recap</title>
      <dc:creator>Casey Brooks</dc:creator>
      <pubDate>Wed, 11 Sep 2019 17:11:21 +0000</pubDate>
      <link>https://dev.to/cjbrooks12/monthly-orchid-update-summer-recap-2gbl</link>
      <guid>https://dev.to/cjbrooks12/monthly-orchid-update-summer-recap-2gbl</guid>
      <description>&lt;p&gt;Sorry for the absence, it's been a very busy summer for me! Lots of stuff going on both for me and for Orchid, so stick around to see what's coming up!&lt;/p&gt;

&lt;p&gt;This is a monthly newsletter around Orchid, the newest and best static site generator for the JVM. There is a growing need to keep the community up-to-date on all the happenings around Orchid, and here I will share Orchid's progress during the previous month! Follow along with this series to stay on top of Orchid's newest features, track adoption on Github, and see who's using Orchid!&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/orchidhq" rel="noopener noreferrer"&gt;
        orchidhq
      &lt;/a&gt; / &lt;a href="https://github.com/orchidhq/Orchid" rel="noopener noreferrer"&gt;
        Orchid
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Build and deploy beautiful documentation sites that grow with you
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/" rel="nofollow noopener noreferrer"&gt;
    &lt;img width="460" src="https://camo.githubusercontent.com/bb0153811a1ae7ec58b4836825a32c2982f1f6822f0fbe8bd3cb2e7a7b038eeb/68747470733a2f2f6f726368696468712e6769746875622e696f2f4f72636869642f6173736574732f7376672f6f72636869642f6c6f676f5f746f705f6c696768742e737667" title="Orchid" alt="Orchid"&gt;
  &lt;/a&gt;
  &lt;br&gt;
  &lt;strong&gt;Build and deploy beautiful documentation sites that grow with you&lt;/strong&gt;
&lt;/p&gt;




&lt;p&gt;
  &lt;a href="https://search.maven.org/artifact/io.github.copper-leaf.orchid/orchid-core" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="Maven Central (Releases)" src="https://camo.githubusercontent.com/a65a457ca8dbc4c986ffe4c6c22f1d3520afc40f9d1f9a04d881a32a205bb15f/68747470733a2f2f696d672e736869656c64732e696f2f6d6176656e2d63656e7472616c2f762f696f2e6769746875622e636f707065722d6c6561662e6f72636869642f6f72636869642d636f72653f6c6162656c3d52656c65617365"&gt;
  &lt;/a&gt;
  &lt;a href="https://s01.oss.sonatype.org/content/repositories/snapshots/io/github/copper-leaf/orchid/orchid-core" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="Sonatype Nexus (Snapshots)" src="https://camo.githubusercontent.com/b6adbc1d22e7babe344faa6e9ea3792a04c54bc9ef5dea80a105e1cc065d38d0/68747470733a2f2f696d672e736869656c64732e696f2f6e657875732f732f696f2e6769746875622e636f707065722d6c6561662e6f72636869642f6f72636869642d636f72653f6c6162656c3d536e617073686f74267365727665723d68747470732533412532462532467330312e6f73732e736f6e61747970652e6f7267"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/orchidhq/Orchid/actions/workflows/push_dev.yml" rel="noopener noreferrer"&gt;
    &lt;img src="https://github.com/orchidhq/Orchid/actions/workflows/push_dev.yml/badge.svg?branch=dev" title="Build Status" alt="Build Status"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/orchidhq/Orchid/License.md" rel="noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/1b0c7e4911720d0444c16a1ffd145a039f14a1a7305362ab51184f757a4dd6bc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d47504c25323076332d626c75652e737667" title="License: GPL-3.0" alt="License: GPL-3.0"&gt;
  &lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/c27002240e14f6ffa0e6f0bf6f172016e14e366360a02cd5d7193d37415feee1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4a444b2d382d2d31362d7265643f7374796c653d666c6174266c6f676f3d6a617661"&gt;&lt;img src="https://camo.githubusercontent.com/c27002240e14f6ffa0e6f0bf6f172016e14e366360a02cd5d7193d37415feee1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4a444b2d382d2d31362d7265643f7374796c653d666c6174266c6f676f3d6a617661" title="JDK: 8-16" alt="JDK: 8-16"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart" rel="nofollow noopener noreferrer"&gt;Quick-Start&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/wiki/learn" rel="nofollow noopener noreferrer"&gt;Tutorials&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/showcase" rel="nofollow noopener noreferrer"&gt;Showcase&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://gitter.im/JavaEden/Orchid" rel="nofollow noopener noreferrer"&gt;Support&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick-Start&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart#gradle" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Forchidhq%2FOrchid%2FHEAD%2Fdocs%2Fsrc%2Forchid%2Fresources%2Fassets%2Fsvg%2Fgradle.svg" title="Gradle" alt="Gradle" width="200" height="50"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart#maven" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Forchidhq%2FOrchid%2FHEAD%2Fdocs%2Fsrc%2Forchid%2Fresources%2Fassets%2Fsvg%2Fmaven.svg" title="Maven" alt="Maven" width="200" height="50"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart#sbt" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Forchidhq%2FOrchid%2FHEAD%2Fdocs%2Fsrc%2Forchid%2Fresources%2Fassets%2Fsvg%2Fsbt.svg" title="SBT" alt="SBT" width="200" height="50"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;a href="https://app.netlify.com/start/deploy?repository=https://github.com/orchidhq/OrchidStarter" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://camo.githubusercontent.com/de47084e2d5a8948486a2238f1517e465560ec293c4a2cba80020472f8d5c80a/68747470733a2f2f7777772e6e65746c6966792e636f6d2f696d672f6465706c6f792f627574746f6e2e737667" title="Deploy to Netlify" alt="Deploy to Netlify" width="200" height="50" class="js-gh-image-fallback"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;p&gt;Orchid 0.X.X versions are published to &lt;a href="https://jcenter.bintray.com/" rel="nofollow noopener noreferrer"&gt;JCenter&lt;/a&gt; at artifact coordinates like &lt;code&gt;io.github.javaeden.orchid:OrchidCore:0.21.1&lt;/code&gt; or &lt;code&gt;io.github.javaeden.orchid:OrchidWiki:0.21.1&lt;/code&gt;. JCenter is deprecated, and once Orchid 1.0.0 is published, so will all 0.X.X versions.&lt;/p&gt;

&lt;p&gt;Starting with version 1.0.0, Orchid will be published to &lt;a href="https://repo1.maven.org/maven2/" rel="nofollow noopener noreferrer"&gt;MavenCentral&lt;/a&gt; under new artifact coordinates, like &lt;code&gt;io.github.copper-leaf.orchid:orchid-core:1.0.0&lt;/code&gt; or &lt;code&gt;io.github.copper-leaf.orchid:orchid-wiki-feature:1.0.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In-development snapshot versions will be available in &lt;a href="https://s01.oss.sonatype.org/content/repositories/snapshots/" rel="nofollow noopener noreferrer"&gt;Sonatype's new (s01) snapshots repository&lt;/a&gt;. Snapshots are published after every successful build on the &lt;code&gt;dev&lt;/code&gt; branch.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Documentation&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Orchid's User Manual will walk you through the main features of Orchid and give you a deeper understanding of each topic
and feature.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tutorials&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;There are several tutorials designed to walk you through building an Orchid site from scratch. The source for all
tutorials can also be found in the &lt;a href="https://github.com/orchidhq/OrchidTutorials" rel="noopener noreferrer"&gt;OrchidTutorials repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://orchidhq.github.io/Orchid/wiki/learn" rel="nofollow noopener noreferrer"&gt;Tutorials&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Showcase&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;View…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/orchidhq/Orchid" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h1&gt;
  
  
  On Github
&lt;/h1&gt;

&lt;p&gt;As of the time of writing, Orchid is at 233 stars on Github, thank you so much for all the support! And a special thanks goes to the following individuals who submitted pull requests to Orchid!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Sumo99" rel="noopener noreferrer"&gt;Sumo99&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jmfayard" rel="noopener noreferrer"&gt;Jean-Michel Fayard&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've also been preparing for &lt;a href="https://hacktoberfest.digitalocean.com" rel="noopener noreferrer"&gt;Hacktoberfest&lt;/a&gt;, which is coming up in less than a month. I've curated a bunch of new issues labeled for &lt;a href="https://github.com/JavaEden/Orchid/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A" rel="noopener noreferrer"&gt;first-time contributors&lt;/a&gt;, how about getting a head-start on Hacktoberfest and helping out with some of these issues? 😎 Feel free to hold off on your PR until Hacktoberfest officially begins, just comment on the issue so others know what you're working on.&lt;/p&gt;

&lt;h1&gt;
  
  
  What's New?
&lt;/h1&gt;

&lt;p&gt;Orchid is now at version &lt;a href="https://github.com/JavaEden/Orchid/releases/tag/0.17.4" rel="noopener noreferrer"&gt;0.17.4&lt;/a&gt;. 0.17.0, which came out at the end of May, was a really big update and included a handful of new "integrations" projects. These integrations make it easy to create wikis from Github and Gitlab wikis, and to publish your site to Gitlab Pages and Bitbucket Cloud (in addition to the already-supported Github and Netlify publishers). &lt;/p&gt;

&lt;h1&gt;
  
  
  In Progress
&lt;/h1&gt;

&lt;p&gt;Y'all. Some big things are just up the road for Orchid. Really big. Like, &lt;em&gt;Texas-sized big&lt;/em&gt;. I'm talking about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A completely redesigned documentation site (you can preview it &lt;a href="https://new-docs--orchid.netlify.com" rel="noopener noreferrer"&gt;here&lt;/a&gt;, I'd greatly appreciate any feedback on it!)&lt;/li&gt;
&lt;li&gt;Mutli-module code documentation&lt;/li&gt;
&lt;li&gt;Completely decoupling code documentation from Orchid with &lt;a href="https://github.com/copper-leaf/kodiak" rel="noopener noreferrer"&gt;Kodiak&lt;/a&gt;, to vastly simplify the process of supporting new languages and ensure consistency of usage across all languages&lt;/li&gt;
&lt;li&gt;Some much-needed refactoring of core Orchid functionality&lt;/li&gt;
&lt;li&gt;A new framework for automated testing of Orchid plugins and themes&lt;/li&gt;
&lt;li&gt;Migrating to GitHub Actions for continuous releases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All this will be coming in the upcoming 0.18.0 release. While I don't want to put a date on this release (I don't want to release it until it's 100% ready-to-go), I can say that I'm nearly code-complete with the changes I wanted to add. From here, it's mostly just adding the finishing touches to the new site, documenting the new functionality, and testing everything thoroughly. &lt;/p&gt;




&lt;p&gt;Are you interested in getting started with Orchid? There simply is no better way to manage all the documentation for your project, and I'd love to help you get set up! &lt;/p&gt;

&lt;p&gt;If you have an open-source project that needs docs, are building a new portfolio, or are building any other kind of static site, I want to work with you to get you set up with Orchid! Comment on this post, send me a PM here on Dev.to, reach out on &lt;a href="https://gitter.im/JavaEden/Orchid" rel="noopener noreferrer"&gt;Gitter&lt;/a&gt;, or &lt;a href="https://www.caseyjbrooks.com/contact/" rel="noopener noreferrer"&gt;contact me here&lt;/a&gt; and I will be with you every step of the way.&lt;/p&gt;

&lt;p&gt;And as always, let me know if you start using Orchid so I can feature you in next month's update!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>orchid</category>
      <category>static</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Orchid Newsletter: In Bloom</title>
      <dc:creator>Casey Brooks</dc:creator>
      <pubDate>Tue, 07 May 2019 21:08:12 +0000</pubDate>
      <link>https://dev.to/cjbrooks12/monthly-orchid-update-in-bloom-21l8</link>
      <guid>https://dev.to/cjbrooks12/monthly-orchid-update-in-bloom-21l8</guid>
      <description>&lt;p&gt;April has just ended, which means summer is coming! April in Texas means lots of rain, the first hot days of summer, and bluebonnets in bloom along every highway. It's also been a relatively slow month for Orchid, but lots of really cool stuff is just about to bloom here too!&lt;/p&gt;

&lt;p&gt;This is a monthly newsletter around Orchid, the newest and best static site generator for the JVM. There is a growing need to keep the community up-to-date on all the happenings around Orchid, and here I will share Orchid's progress during the previous month! Follow along with this series to stay on top of Orchid's newest features, track adoption on Github, and see who's using Orchid!&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/orchidhq" rel="noopener noreferrer"&gt;
        orchidhq
      &lt;/a&gt; / &lt;a href="https://github.com/orchidhq/Orchid" rel="noopener noreferrer"&gt;
        Orchid
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Build and deploy beautiful documentation sites that grow with you
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/" rel="nofollow noopener noreferrer"&gt;
    &lt;img width="460" src="https://camo.githubusercontent.com/bb0153811a1ae7ec58b4836825a32c2982f1f6822f0fbe8bd3cb2e7a7b038eeb/68747470733a2f2f6f726368696468712e6769746875622e696f2f4f72636869642f6173736574732f7376672f6f72636869642f6c6f676f5f746f705f6c696768742e737667" title="Orchid" alt="Orchid"&gt;
  &lt;/a&gt;
  &lt;br&gt;
  &lt;strong&gt;Build and deploy beautiful documentation sites that grow with you&lt;/strong&gt;
&lt;/p&gt;




&lt;p&gt;
  &lt;a href="https://search.maven.org/artifact/io.github.copper-leaf.orchid/orchid-core" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="Maven Central (Releases)" src="https://camo.githubusercontent.com/a65a457ca8dbc4c986ffe4c6c22f1d3520afc40f9d1f9a04d881a32a205bb15f/68747470733a2f2f696d672e736869656c64732e696f2f6d6176656e2d63656e7472616c2f762f696f2e6769746875622e636f707065722d6c6561662e6f72636869642f6f72636869642d636f72653f6c6162656c3d52656c65617365"&gt;
  &lt;/a&gt;
  &lt;a href="https://s01.oss.sonatype.org/content/repositories/snapshots/io/github/copper-leaf/orchid/orchid-core" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="Sonatype Nexus (Snapshots)" src="https://camo.githubusercontent.com/b6adbc1d22e7babe344faa6e9ea3792a04c54bc9ef5dea80a105e1cc065d38d0/68747470733a2f2f696d672e736869656c64732e696f2f6e657875732f732f696f2e6769746875622e636f707065722d6c6561662e6f72636869642f6f72636869642d636f72653f6c6162656c3d536e617073686f74267365727665723d68747470732533412532462532467330312e6f73732e736f6e61747970652e6f7267"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/orchidhq/Orchid/actions/workflows/push_dev.yml" rel="noopener noreferrer"&gt;
    &lt;img src="https://github.com/orchidhq/Orchid/actions/workflows/push_dev.yml/badge.svg?branch=dev" title="Build Status" alt="Build Status"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/orchidhq/Orchid/License.md" rel="noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/1b0c7e4911720d0444c16a1ffd145a039f14a1a7305362ab51184f757a4dd6bc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d47504c25323076332d626c75652e737667" title="License: GPL-3.0" alt="License: GPL-3.0"&gt;
  &lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/c27002240e14f6ffa0e6f0bf6f172016e14e366360a02cd5d7193d37415feee1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4a444b2d382d2d31362d7265643f7374796c653d666c6174266c6f676f3d6a617661"&gt;&lt;img src="https://camo.githubusercontent.com/c27002240e14f6ffa0e6f0bf6f172016e14e366360a02cd5d7193d37415feee1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4a444b2d382d2d31362d7265643f7374796c653d666c6174266c6f676f3d6a617661" title="JDK: 8-16" alt="JDK: 8-16"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart" rel="nofollow noopener noreferrer"&gt;Quick-Start&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/wiki/learn" rel="nofollow noopener noreferrer"&gt;Tutorials&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/showcase" rel="nofollow noopener noreferrer"&gt;Showcase&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://gitter.im/JavaEden/Orchid" rel="nofollow noopener noreferrer"&gt;Support&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick-Start&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart#gradle" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Forchidhq%2FOrchid%2FHEAD%2Fdocs%2Fsrc%2Forchid%2Fresources%2Fassets%2Fsvg%2Fgradle.svg" title="Gradle" alt="Gradle" width="200" height="50"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart#maven" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Forchidhq%2FOrchid%2FHEAD%2Fdocs%2Fsrc%2Forchid%2Fresources%2Fassets%2Fsvg%2Fmaven.svg" title="Maven" alt="Maven" width="200" height="50"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart#sbt" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Forchidhq%2FOrchid%2FHEAD%2Fdocs%2Fsrc%2Forchid%2Fresources%2Fassets%2Fsvg%2Fsbt.svg" title="SBT" alt="SBT" width="200" height="50"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;a href="https://app.netlify.com/start/deploy?repository=https://github.com/orchidhq/OrchidStarter" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://camo.githubusercontent.com/de47084e2d5a8948486a2238f1517e465560ec293c4a2cba80020472f8d5c80a/68747470733a2f2f7777772e6e65746c6966792e636f6d2f696d672f6465706c6f792f627574746f6e2e737667" title="Deploy to Netlify" alt="Deploy to Netlify" width="200" height="50" class="js-gh-image-fallback"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;p&gt;Orchid 0.X.X versions are published to &lt;a href="https://jcenter.bintray.com/" rel="nofollow noopener noreferrer"&gt;JCenter&lt;/a&gt; at artifact coordinates like &lt;code&gt;io.github.javaeden.orchid:OrchidCore:0.21.1&lt;/code&gt; or &lt;code&gt;io.github.javaeden.orchid:OrchidWiki:0.21.1&lt;/code&gt;. JCenter is deprecated, and once Orchid 1.0.0 is published, so will all 0.X.X versions.&lt;/p&gt;

&lt;p&gt;Starting with version 1.0.0, Orchid will be published to &lt;a href="https://repo1.maven.org/maven2/" rel="nofollow noopener noreferrer"&gt;MavenCentral&lt;/a&gt; under new artifact coordinates, like &lt;code&gt;io.github.copper-leaf.orchid:orchid-core:1.0.0&lt;/code&gt; or &lt;code&gt;io.github.copper-leaf.orchid:orchid-wiki-feature:1.0.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In-development snapshot versions will be available in &lt;a href="https://s01.oss.sonatype.org/content/repositories/snapshots/" rel="nofollow noopener noreferrer"&gt;Sonatype's new (s01) snapshots repository&lt;/a&gt;. Snapshots are published after every successful build on the &lt;code&gt;dev&lt;/code&gt; branch.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Documentation&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Orchid's User Manual will walk you through the main features of Orchid and give you a deeper understanding of each topic
and feature.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tutorials&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;There are several tutorials designed to walk you through building an Orchid site from scratch. The source for all
tutorials can also be found in the &lt;a href="https://github.com/orchidhq/OrchidTutorials" rel="noopener noreferrer"&gt;OrchidTutorials repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://orchidhq.github.io/Orchid/wiki/learn" rel="nofollow noopener noreferrer"&gt;Tutorials&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Showcase&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;View…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/orchidhq/Orchid" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h1&gt;
  
  
  On Github
&lt;/h1&gt;

&lt;p&gt;As of the time of writing, Orchid is at 196 stars on Github. Just 4 more stars until the next major milestone of 200, will you help us get there?&lt;/p&gt;

&lt;h1&gt;
  
  
  What's New?
&lt;/h1&gt;

&lt;p&gt;Orchid is now at version &lt;a href="https://github.com/JavaEden/Orchid/releases/tag/0.16.10" rel="noopener noreferrer"&gt;0.16.10&lt;/a&gt;, with a few minor updates released throughout the month. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Orchid no longer includes any Lombok code, for a more stable codebase and a better roadmap for converting the remaining Java code to Kotlin. &lt;/li&gt;
&lt;li&gt;Relative base URLs are now supported&lt;/li&gt;
&lt;li&gt;Adds &lt;code&gt;baseUrl&lt;/code&gt; and &lt;code&gt;homepageUrl&lt;/code&gt; filters to be more tolerant when formatting ad-hoc URLs&lt;/li&gt;
&lt;li&gt;Updates to Pebble 3.0.9, which includes the &lt;a href="https://pebbletemplates.io/wiki/tag/embed/" rel="noopener noreferrer"&gt;&lt;code&gt;embed&lt;/code&gt;&lt;/a&gt; tag I contributed to that project. This will go a long way in improving theme development in Orchid&lt;/li&gt;
&lt;li&gt;Improved error-handling&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  In Progress
&lt;/h1&gt;

&lt;p&gt;I've been pretty busy in April and haven't had as much time to dedicate to Orchid development. One of my other hobbies is woodworking, and I've been taking some time in this nice weather to build a new coffee table as a late Christmas present to my wife. I'm pretty proud of my work on it, it's teaching me some skills that are valuable in software development, and you can keep an eye out for a blog post on that! Here's my progress on this build so far:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhmsitizwusfor9fihtf4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhmsitizwusfor9fihtf4.png" alt="Design in SketchUp" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdf3enfgl81npxveyzms9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdf3enfgl81npxveyzms9.jpg" alt="Tabletop in Progress" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But despite that, I'm also making some great progress toward the next major release, 0.17.0! This will be a significant refactoring and improvement of the wiki and deployment features in Orchid to allow better integrations with all the major Git hosting platforms and pave the way for easier integration of similar features in the future.&lt;/p&gt;

&lt;p&gt;It's a pretty significant change and is taking a lot of work, but I have a goal of finally getting it out this month! Here's the full list of features that you can expect in this release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Netlify deploys optimized for really large sites, and able to upload &lt;a href="https://www.netlify.com/products/functions/" rel="noopener noreferrer"&gt;serverless Functions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Deploy site to GitHub, Bitbucket, GitLab, or Azure DevOps (currently, only GitHub is supported)&lt;/li&gt;
&lt;li&gt;Import wiki content directly from GitHub Wikis&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Are you interested in getting started with Orchid? There simply is no better way to manage all the documentation for your project, and I'd love to help you get set up! &lt;/p&gt;

&lt;p&gt;If you have an open-source project that needs docs, are building a new portfolio, or are building any other kind of static site, I want to work with you to get you set up with Orchid! Comment on this post, send me a PM here on Dev.to, or &lt;a href="https://www.caseyjbrooks.com/contact/" rel="noopener noreferrer"&gt;contact me here&lt;/a&gt; and I will be with you every step of the way.&lt;/p&gt;

&lt;p&gt;And as always, let me know if you start using Orchid so I can feature you in next month's update!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>orchid</category>
      <category>static</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Orchid Newsletter: March Madness</title>
      <dc:creator>Casey Brooks</dc:creator>
      <pubDate>Wed, 03 Apr 2019 15:56:32 +0000</pubDate>
      <link>https://dev.to/cjbrooks12/monthly-orchid-update-march-madness-1622</link>
      <guid>https://dev.to/cjbrooks12/monthly-orchid-update-march-madness-1622</guid>
      <description>&lt;p&gt;I love watching basketball, which makes March one of the best months of the year as the NCAA National Championship, March Madness, takes place. My top pick, Michigan State, has already defeated Duke and is well on their way to becoming the National Champions!&lt;/p&gt;

&lt;p&gt;It has also been quite a "mad" month for Orchid too, with lots of big changes in progress. From being able to publish your site to Gitlab and Bitbucket, to integration with Github Wikis, and a re-theming of the documentation site all underway, it's safe to say that I've been quite busy!&lt;/p&gt;

&lt;p&gt;This is a monthly newsletter around Orchid, the newest and best static site generator for the JVM. There is a growing need to keep the community up-to-date on all the happenings around Orchid, and here I will share Orchid's progress during the previous month! Follow along with this series to stay on top of Orchid's newest features, track adoption on Github, and see who's using Orchid!&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/orchidhq" rel="noopener noreferrer"&gt;
        orchidhq
      &lt;/a&gt; / &lt;a href="https://github.com/orchidhq/Orchid" rel="noopener noreferrer"&gt;
        Orchid
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Build and deploy beautiful documentation sites that grow with you
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/" rel="nofollow noopener noreferrer"&gt;
    &lt;img width="460" src="https://camo.githubusercontent.com/bb0153811a1ae7ec58b4836825a32c2982f1f6822f0fbe8bd3cb2e7a7b038eeb/68747470733a2f2f6f726368696468712e6769746875622e696f2f4f72636869642f6173736574732f7376672f6f72636869642f6c6f676f5f746f705f6c696768742e737667" title="Orchid" alt="Orchid"&gt;
  &lt;/a&gt;
  &lt;br&gt;
  &lt;strong&gt;Build and deploy beautiful documentation sites that grow with you&lt;/strong&gt;
&lt;/p&gt;




&lt;p&gt;
  &lt;a href="https://search.maven.org/artifact/io.github.copper-leaf.orchid/orchid-core" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="Maven Central (Releases)" src="https://camo.githubusercontent.com/a65a457ca8dbc4c986ffe4c6c22f1d3520afc40f9d1f9a04d881a32a205bb15f/68747470733a2f2f696d672e736869656c64732e696f2f6d6176656e2d63656e7472616c2f762f696f2e6769746875622e636f707065722d6c6561662e6f72636869642f6f72636869642d636f72653f6c6162656c3d52656c65617365"&gt;
  &lt;/a&gt;
  &lt;a href="https://s01.oss.sonatype.org/content/repositories/snapshots/io/github/copper-leaf/orchid/orchid-core" rel="nofollow noopener noreferrer"&gt;
    &lt;img alt="Sonatype Nexus (Snapshots)" src="https://camo.githubusercontent.com/b6adbc1d22e7babe344faa6e9ea3792a04c54bc9ef5dea80a105e1cc065d38d0/68747470733a2f2f696d672e736869656c64732e696f2f6e657875732f732f696f2e6769746875622e636f707065722d6c6561662e6f72636869642f6f72636869642d636f72653f6c6162656c3d536e617073686f74267365727665723d68747470732533412532462532467330312e6f73732e736f6e61747970652e6f7267"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/orchidhq/Orchid/actions/workflows/push_dev.yml" rel="noopener noreferrer"&gt;
    &lt;img src="https://github.com/orchidhq/Orchid/actions/workflows/push_dev.yml/badge.svg?branch=dev" title="Build Status" alt="Build Status"&gt;
  &lt;/a&gt;
  &lt;a href="https://github.com/orchidhq/Orchid/License.md" rel="noopener noreferrer"&gt;
    &lt;img src="https://camo.githubusercontent.com/1b0c7e4911720d0444c16a1ffd145a039f14a1a7305362ab51184f757a4dd6bc/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d47504c25323076332d626c75652e737667" title="License: GPL-3.0" alt="License: GPL-3.0"&gt;
  &lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/c27002240e14f6ffa0e6f0bf6f172016e14e366360a02cd5d7193d37415feee1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4a444b2d382d2d31362d7265643f7374796c653d666c6174266c6f676f3d6a617661"&gt;&lt;img src="https://camo.githubusercontent.com/c27002240e14f6ffa0e6f0bf6f172016e14e366360a02cd5d7193d37415feee1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4a444b2d382d2d31362d7265643f7374796c653d666c6174266c6f676f3d6a617661" title="JDK: 8-16" alt="JDK: 8-16"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart" rel="nofollow noopener noreferrer"&gt;Quick-Start&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/wiki/learn" rel="nofollow noopener noreferrer"&gt;Tutorials&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://orchidhq.github.io/Orchid/showcase" rel="nofollow noopener noreferrer"&gt;Showcase&lt;/a&gt;
  &lt;span&gt;•&lt;/span&gt;
  &lt;a href="https://gitter.im/JavaEden/Orchid" rel="nofollow noopener noreferrer"&gt;Support&lt;/a&gt;
&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick-Start&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart#gradle" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Forchidhq%2FOrchid%2FHEAD%2Fdocs%2Fsrc%2Forchid%2Fresources%2Fassets%2Fsvg%2Fgradle.svg" title="Gradle" alt="Gradle" width="200" height="50"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart#maven" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Forchidhq%2FOrchid%2FHEAD%2Fdocs%2Fsrc%2Forchid%2Fresources%2Fassets%2Fsvg%2Fmaven.svg" title="Maven" alt="Maven" width="200" height="50"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started/quickstart#sbt" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Forchidhq%2FOrchid%2FHEAD%2Fdocs%2Fsrc%2Forchid%2Fresources%2Fassets%2Fsvg%2Fsbt.svg" title="SBT" alt="SBT" width="200" height="50"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;br&gt;
&lt;a href="https://app.netlify.com/start/deploy?repository=https://github.com/orchidhq/OrchidStarter" rel="nofollow noopener noreferrer"&gt;&lt;br&gt;
  &lt;img src="https://camo.githubusercontent.com/de47084e2d5a8948486a2238f1517e465560ec293c4a2cba80020472f8d5c80a/68747470733a2f2f7777772e6e65746c6966792e636f6d2f696d672f6465706c6f792f627574746f6e2e737667" title="Deploy to Netlify" alt="Deploy to Netlify" width="200" height="50" class="js-gh-image-fallback"&gt;&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;p&gt;Orchid 0.X.X versions are published to &lt;a href="https://jcenter.bintray.com/" rel="nofollow noopener noreferrer"&gt;JCenter&lt;/a&gt; at artifact coordinates like &lt;code&gt;io.github.javaeden.orchid:OrchidCore:0.21.1&lt;/code&gt; or &lt;code&gt;io.github.javaeden.orchid:OrchidWiki:0.21.1&lt;/code&gt;. JCenter is deprecated, and once Orchid 1.0.0 is published, so will all 0.X.X versions.&lt;/p&gt;

&lt;p&gt;Starting with version 1.0.0, Orchid will be published to &lt;a href="https://repo1.maven.org/maven2/" rel="nofollow noopener noreferrer"&gt;MavenCentral&lt;/a&gt; under new artifact coordinates, like &lt;code&gt;io.github.copper-leaf.orchid:orchid-core:1.0.0&lt;/code&gt; or &lt;code&gt;io.github.copper-leaf.orchid:orchid-wiki-feature:1.0.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In-development snapshot versions will be available in &lt;a href="https://s01.oss.sonatype.org/content/repositories/snapshots/" rel="nofollow noopener noreferrer"&gt;Sonatype's new (s01) snapshots repository&lt;/a&gt;. Snapshots are published after every successful build on the &lt;code&gt;dev&lt;/code&gt; branch.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Documentation&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Orchid's User Manual will walk you through the main features of Orchid and give you a deeper understanding of each topic
and feature.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://orchidhq.github.io/Orchid/wiki/user-manual/getting-started" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tutorials&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;There are several tutorials designed to walk you through building an Orchid site from scratch. The source for all
tutorials can also be found in the &lt;a href="https://github.com/orchidhq/OrchidTutorials" rel="noopener noreferrer"&gt;OrchidTutorials repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://orchidhq.github.io/Orchid/wiki/learn" rel="nofollow noopener noreferrer"&gt;Tutorials&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Showcase&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;View…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/orchidhq/Orchid" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h1&gt;
  
  
  On Github
&lt;/h1&gt;

&lt;p&gt;As of the time of writing, Orchid is at 175 stars on Github. And this month, in particular, I have been absolutely blown away at the growth on Github. With 4 pull requests by 3 contributors, numerous issues opened and questions asked across Github, Twitter, and Gitter, and more than FOUR TIMES as many downloads on Bintray this month as last month, Orchid is seeing unprecedented growth that I could never have imagined. &lt;/p&gt;

&lt;p&gt;Thank you all so much, I certainly could not keep working this hard without your incredible support! But these successes are entirely because of you, and I love to give credit where it is due, so let's drill into this a bit deeper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contributions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/alejandrohdezma" rel="noopener noreferrer"&gt;@alejandrohdezma&lt;/a&gt; helped out to fix a bug using the wrong FontAwesome icon (&lt;a href="https://github.com/JavaEden/Orchid/pull/239" rel="noopener noreferrer"&gt;#239&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Sumo99" rel="noopener noreferrer"&gt;@ Sumo99&lt;/a&gt; got rid of the last of Lombok, paving the way for a pure-Kotlin future! They also helped remove Google Plus social links now that the service is officially dea. (&lt;a href="https://github.com/JavaEden/Orchid/pull/243" rel="noopener noreferrer"&gt;#243&lt;/a&gt; and &lt;a href="https://github.com/JavaEden/Orchid/pull/249" rel="noopener noreferrer"&gt;#249&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dkowis" rel="noopener noreferrer"&gt;@ dkowis&lt;/a&gt; fixed a broken documentation link (&lt;a href="https://github.com/JavaEden/Orchid/pull/252" rel="noopener noreferrer"&gt;#252&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4x Downloads Increase
&lt;/h2&gt;

&lt;p&gt;The 31-day period ending February 28th saw a bit shy of 700 downloads.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff0a4sp6eq3l4ztfyp6ic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff0a4sp6eq3l4ztfyp6ic.png" alt="February downloads" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The stats show that downloads really started to take off around the end of the month, which is when I published the tutorial on how to use Orchid to document a Kotlin project. I would highly recommend you check it out if you haven't already.&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/cjbrooks12/how-to-document-a-kotlin-project-edc" class="crayons-story__hidden-navigation-link"&gt;How To Document A Kotlin Project&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/cjbrooks12" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F66227%2F6e81450b-870b-492c-b7ad-ae964c7c0006.jpeg" alt="cjbrooks12 profile" class="crayons-avatar__image" width="460" height="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/cjbrooks12" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Casey Brooks
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Casey Brooks
                
                
              
              &lt;div id="story-author-preview-content-83362" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/cjbrooks12" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F66227%2F6e81450b-870b-492c-b7ad-ae964c7c0006.jpeg" class="crayons-avatar__image" alt="" width="460" height="460"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Casey Brooks&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/cjbrooks12/how-to-document-a-kotlin-project-edc" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 18 '19&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/cjbrooks12/how-to-document-a-kotlin-project-edc" id="article-link-83362"&gt;
          How To Document A Kotlin Project
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/kotlin"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;kotlin&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/documentation"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;documentation&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/orchid"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;orchid&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/cjbrooks12/how-to-document-a-kotlin-project-edc" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;43&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/cjbrooks12/how-to-document-a-kotlin-project-edc#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              5&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            14 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;Meanwhile, The 31-day period ending March 31st saw more than 2600 downloads! And questions from y'all on how to use it have increased to match, and I'm so excited to be able to help solve your problems with Orchid!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89h8grf635y6yhmrse0l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89h8grf635y6yhmrse0l.png" alt="March downloads" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All of these statistics are freely available on Bintray, &lt;a href="https://bintray.com/beta/#/javaeden/Orchid/OrchidCore?tab=statistics" rel="noopener noreferrer"&gt;go here&lt;/a&gt; to check it out for yourself.&lt;/p&gt;

&lt;h1&gt;
  
  
  What's New?
&lt;/h1&gt;

&lt;p&gt;Orchid is currently at version &lt;a href="https://github.com/JavaEden/Orchid/releases/tag/0.16.7" rel="noopener noreferrer"&gt;0.16.7&lt;/a&gt;. There have been no major changes since last month, mostly just a series of bugfixes on the Copper theme and minor usability improvements.&lt;/p&gt;

&lt;h1&gt;
  
  
  Coming Soon
&lt;/h1&gt;

&lt;h2&gt;
  
  
  New Docs
&lt;/h2&gt;

&lt;p&gt;Orchid's docs are getting a reboot! When I started work on Orchid, Bootstrap was the only CSS framework I knew of. I had no idea there were so many great options out there, and ultimately I have come to really enjoy Bulma for its simplicity and flexibility. &lt;/p&gt;

&lt;p&gt;And so, I've been building the "Copper" theme, based on Bulma, to serve as the home for all of Orchid's own documentation, and also of its supplemental libraries. Here's a preview:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://new-docs--orchid.netlify.com/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxl7oo2y3hlj4semkejh7.png" alt="New Docs" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also coming with the new theme will be a major overhaul in the &lt;em&gt;content&lt;/em&gt; on the docs site, as I continue to iterate upon the docs and figure out the best way to present the information to you. &lt;/p&gt;

&lt;h2&gt;
  
  
  More Integrations
&lt;/h2&gt;

&lt;p&gt;From the very beginning, Orchid was created to be infinitely flexible, able to work with a wide variety of different systems and content structures, but until now it has been fairly closely tied to just Netlify and GitHub. I myself regularly use Microsft Azure and BitBucket at work and want to use Orchid in those places, and I can imagine y'all do as well.&lt;/p&gt;

&lt;p&gt;So work is currently under way to make it easier to integrate Orchid into those different Git platforms, so you do not need to change your current processes to fully utilize Orchid's power! Soon, you'll be able to use the native Wikis on Github, Bitbucket, Gitlab, and Azure DevOps as a headless CMS, and you'll also be able to deploy directly to their static hosting platforms and create releases!&lt;/p&gt;

&lt;h1&gt;
  
  
  Get Involved
&lt;/h1&gt;

&lt;p&gt;You don't have to be an expert in Java, Kotlin, Orchid, or anything else to help out the Orchid project. There are a number of ways you can contribute right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fixing typos and improving the clarity of documentation articles&lt;/li&gt;
&lt;li&gt;converting Java classes to Kotlin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, I am currently looking for more skilled help with a couple specific areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I'm looking for people who currently use the features of the git platforms I'm imtegrating with, to help build their integrations:

&lt;ul&gt;
&lt;li&gt;GitLab

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/JavaEden/Orchid/blob/features/integrations/integrations/OrchidGitlab/src/main/kotlin/com/eden/orchid/gitlab/wiki/GitlabWikiAdapter.kt" rel="noopener noreferrer"&gt;Wiki adapter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/JavaEden/Orchid/blob/features/integrations/integrations/OrchidGitlab/src/main/kotlin/com/eden/orchid/gitlab/publication/GitlabPagesPublisher.kt" rel="noopener noreferrer"&gt;GitLab Pages Publisher&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Bitbucket

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/JavaEden/Orchid/blob/features/integrations/integrations/OrchidBitbucket/src/main/kotlin/com/eden/orchid/bitbucket/wiki/BitbucketWikiAdapter.kt" rel="noopener noreferrer"&gt;Wiki adapter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/JavaEden/Orchid/blob/features/integrations/integrations/OrchidBitbucket/src/main/kotlin/com/eden/orchid/bitbucket/publication/BitbucketCloudPublisher.kt" rel="noopener noreferrer"&gt;Bitbucket Cloud Publisher&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Azure DevOps

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/JavaEden/Orchid/blob/features/integrations/integrations/OrchidAzure/src/main/kotlin/com/eden/orchid/azure/wiki/AzureWikiAdapter.kt" rel="noopener noreferrer"&gt;Wiki adapter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would also love the help of a designer to make a really great home page and a new logo for Orchid.&lt;/p&gt;

&lt;p&gt;Please &lt;a href="https://www.caseyjbrooks.com/contact/" rel="noopener noreferrer"&gt;reach out to me&lt;/a&gt; if you're interested in contributing to the project for any of these specific issues, I (and the whole community!) would really appreciate it!&lt;/p&gt;




&lt;p&gt;Are you interested in getting started with Orchid? There simply is no better way to manage all the documentation for your project, and I'd love to help you get set up! &lt;/p&gt;

&lt;p&gt;If you have an open-source project that needs docs, are building a new portfolio, or are building any other kind of static site, I want to work with you to get you set up with Orchid! Comment on this post, send me a PM here on Dev.to, or &lt;a href="https://www.caseyjbrooks.com/contact/" rel="noopener noreferrer"&gt;contact me here&lt;/a&gt; and I will be with you every step of the way.&lt;/p&gt;

&lt;p&gt;And as always, let me know if you start using Orchid so I can feature you in next month's update!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>orchid</category>
      <category>static</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
