Fullstack

Kobweb not only helps you create frontend files for a website, but it also makes it easy to add backend functionality as well.

Similar to how you define pages on the frontend, you can declare annotated API methods on the backend, at which point Kobweb can discover them and handle all the boilerplate around setting up a web server for you.

All backend features also support Kobweb's live reloading experience, so you can iterate on your server while it is running.

Question

Are you an experienced Ktor user who prefers to maintain full control over their backend? If so, consider reading the Using a Custom Backend guide as well, to understand your options.

Declare a full-stack project

In your site's build script, make sure you call configAsKobwebApplication(includeServer = true). Just with that done, you are ready to write server logic for your Kobweb site.

A Kobweb project will always at least have a JavaScript target, representing the frontend, but if you declare an intention to implement a server, that will create a JVM target for you as well. You can add dependencies to this target if you want to make them available to your server code:

import com.varabyte.kobweb.gradle.application.util.configAsKobwebApplication

plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.jetbrains.compose)
    alias(libs.plugins.kobweb.application)
}

/* ... */

kotlin {
    configAsKobwebApplication(includeServer = true)
    sourceSets {
        /* ... */
        jvmMain.dependencies {
            /* ... */
        }
    }
}
site/build.gradle.kts

The easy way to check if everything is set up correctly is to open your project inside IntelliJ IDEA, wait for it to finish indexing, and check that the jvmMain folder is detected as a module (if so, it will be given a special icon and look the same as the jsMain folder):

Kobweb JVM main set up correctly

Define API routes

You can define and annotate methods which will generate server endpoints you can interact with. To add one:

  1. Define your method (optionally suspendable) in a file somewhere under the api package in your jvmMain source directory.
  2. The method should take exactly one argument, an ApiContext.
  3. Annotate it with @Api

For example, here's a simple method that echoes back an argument passed into it:

package com.mysite.api

@Api
suspend fun echo(ctx: ApiContext) {
    // ctx.req is for the incoming request, ctx.res for responding back to the client

    // Params are parsed from the URL, e.g. here "/api/echo?message=..."
    val msg = ctx.req.params["message"] ?: ""
    ctx.res.body = bodyOf(msg)
}
jvmMain/kotlin/com/mysite/api/Echo.kt

After running your project, you can test the endpoint by visiting mysite.com/api/echo?message=hello

You can also trigger the endpoint in your frontend code by using the extension api property added to the kotlinx.browser.window class:

@Page
@Composable
fun ApiDemoPage() {
  val coroutineScope = rememberCoroutineScope()

  Button(onClick = {
    coroutineScope.launch {
      val echoed = window.api.get("echo?message=hello")
          .bodyAsBytes()
          .decodeToString()
      println("Echoed: $echoed")
    }
  }) { Text("Click me") }
}
jsMain/kotlin/com/mysite/pages/ApiDemo.kt

All the HTTP methods are supported (post, put, etc.).

These methods will throw an exception if the request fails for any reason. Note that for every HTTP method, there's a corresponding "try" version that will return null instead (tryPost, tryPut, etc.).

If you know what you're doing, you can, of course, always use window.fetch(...) directly.

Caution

Kobweb API endpoints are prefixed with "api/", so if you want to do a raw window.fetch, be sure to add it in explicitly yourself. In other words, you would window.fetch "/api/echo", not just "echo", above.

Respond to an API request

When you define an API route, you are expected to set a status code for the response, or otherwise it will default to status code 404.

In other words, the following API route stub will return a 404:

@Api
suspend fun error404(ctx: ApiContext) {
}

In contrast, this minimal API route returns an OK status code:

@Api
suspend fun noActionButOk(ctx: ApiContext) {
    ctx.res.status = 200
}
Important

Setting the ctx.res.body property also sets the status code to 200 automatically for you, which is why code in an earlier section worked without setting the status directly. Of course, if you wanted to return a different status code value after setting the body text, you could explicitly set it right after modifying body. For example:

ctx.res.body = bodyOf("...")
ctx.res.status = 201

The design for defaulting to 404 was chosen to allow you to conditionally handle API routes based on input conditions, where early aborts automatically result in the client getting an error.

A very common case is creating an API route that only handles POST requests:

@Api
suspend fun login(ctx: ApiContext) {
    if (ctx.req.method != HttpMethod.POST) return
    // ...
    ctx.res.status = 200
}

Finally, note that you can add headers to your response. A common endpoint that some servers provide is a redirect (302) with an updated URL location. This would look like:

@Api
suspend fun redirect(ctx: ApiContext) {
    if (ctx.req.method != HttpMethod.GET) return
    ctx.res.headers["Location"] = "..."
    ctx.res.status = 302
  
  // Note: A convenience method is provided for this:
  // `ctx.res.setAsRedirect("...", 302)
}

Simple!

Type-safe communication with serialization

With Kobweb, both your frontend and backend are Kotlin code and, moreover, structured using Kotlin Multipaltform.

What this means is it is easy to send type-safe data back and forth between your client and your server. We'll use the following project organization to do this:

  • src
    • commonMain
      • model
    • jsMain
      • pages
    • jvmMain
      • api

Our plan is simple -- put request / response classes in common code, at which point we can reference them in both the frontend and backend in a type-safe matter.

First, add Kotlinx JSON serialization to your project:

[versions]
kobweb = "..."
kotlin = "..."
kotlinx-serialization = "..."

[libraries]
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

[plugins]
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
gradle/libs.versions.toml
plugins {
    /*...*/
    alias(libs.plugins.kotlinx.serialization)
}

kotlin {
    /*...*/
    sourceSets {
        commonMain.dependencies {
            implementation(libs.kotlinx.serialization.json)
        }
    }
}
site/build.gradle.kts

Now, let's use a concrete example to demonstrate this concept. Imagine we owned a real-estate lookup service, with an endpoint called properties. Through it, you can ask for a list of properties in some zipcode matching various search criteria.

Let's quickly design the (definitely oversimplified!) domain models:

@Serializable
data class PropertySummary(
    val id: String,
    val address: String,
    val price: Double,
    val bedrooms: Int,
    val bathrooms: Double,
)

@Serializable
data class PropertySearchRequest(
    val zipCode: String,
    val minPrice: Double? = null,
    val maxPrice: Double? = null,
    val minBedrooms: Int? = null,
)

@Serializable
data class PropertySearchResponse(
    val properties: List<PropertySummary>,
)
commonMain/model/Property.kt
Tip

There is no naming requirement here, but a pretty standard convention is to suffix your request object with Request and your response object with Response, as we did above. When there is a direct 1 to 1 relationship between the request and response, you can see it by noticing XyzRequest and XyzResponse classes living next to each other.

For the next part, we will use the QUERY HTTP verb, which is like GET but supports passing the data in as a body instead of query parameters.

We'll start with the frontend. You'll need to imagine something like collectUserSearchValues exists that pulls values out of a bunch of UI text fields and instantiates a PropertySearchRequest with them:

/*...*/
coroutineScope.launch {
    val searchRequest = collectUserSearchValues()
    val searchResponse = window.api.query(
        "properties",
        bodyOf(Json.encodeToString(searchRequest).encodeAsByteArray()),
    ).bodyAsBytes().decodeToString().let { Json.decodeFromString(it) }
}
jsMain/pages/Search.kt

As you can see, we serialize a request object going in and deserialize a response object coming out.

Next, let's write the backend endpoint. Implementing something like the class PropertiesSearcher is far outside the scope of this article, but it is just a stand-in here for any complex logic you might have on your own backend.

@Api
suspend fun properties(ctx: ApiContext) {
    if (ctx.req.method != HttpMethod.QUERY) return

    val searchRequest = Json.decodeFromString(ctx.req.body!!.text())
    val propertiesSearcher = ctx.data.get<PropertiesSearcher>()
    val foundProperties = propertiesSearcher.find(searchRequest)
    ctx.res.body = bodyOf(Json.encodeToString(PropertySearchResponse(foundProperties)))
}
jvmMain/api/Properties.kt
Note

In the above cases, we use bodyOf both to create bodies for the request object (on the client) and the response object (on the server). There are several flavors of bodyOf methods for wrapping different kinds of content, but by far the most common is the one that wraps a string value, which we have used here.

You can reference the relevant API docs to see the full list of frontend bodyOf and backend bodyOf methods.

And that's it! Many projects, being multi-language, use something like protocol buffers to support sending and receiving type-safe request and response objects over the wire. It is nice that Kotlin Multiplatform gives us a much simpler approach.

But wait... we can do even better!

Kobwebx serialization

Kobweb officially provides a dependency which extends the window.api and window.http objects with JSON-serialization aware methods.

First, add it to your project:

[versions]
kobweb = "..."
kotlin = "..."
kotlinx-serialization = "..."

[libraries]
kobwebx-serialization-kotlinx = { module = "com.varabyte.kobwebx:kobwebx-serialization-kotlinx", version.ref = "kobweb" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

[plugins]
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
gradle/libs.versions.toml
plugins {
    /*...*/
    alias(libs.plugins.kotlinx.serialization)
}

kotlin {
    /*...*/
    sourceSets {
        commonMain.dependencies {
            implementation(libs.kotlinx.serialization.json)
            implementation(libs.kobwebx.serialization.kotlinx)
        }
    }
}
site/build.gradle.kts

And then, let's revisit our code from before, simplifying it:

/*...*/
coroutineScope.launch {
    val searchRequest = collectUserSearchValues()
    val searchResponse = window.api.query<PropertiesSearchRequest>(
        "properties",
        searchRequest
    ).bodyAs<PropertySearchResponse>()
}
jsMain/pages/Search.kt
@Api
suspend fun properties(ctx: ApiContext) {
    if (ctx.req.method != HttpMethod.QUERY) return

    val searchRequest = ctx.req.body!!.decode<PropertiesSearchRequest>()
    val propertiesSearcher = ctx.data.get<PropertiesSearcher>()
    val foundProperties = propertiesSearcher.find(searchRequest)
    ctx.res.body = bodyOf(PropertySearchResponse(foundProperties))
}
jvmMain/api/Properties.kt

We now have the same code flow, but with all awkward boilerplate around juggling bytes and strings and JSON encoding / decoding removed.

Intercept API routes

Kobweb provides a way to intercept all incoming API requests, getting a first chance to handle them before they get passed to the actual API route handler.

To intercept all routes, declare a suspend method annotated with @ApiInterceptor. This method must take a ApiInterceptorContext parameter and return a Response.

You can check the context parameter for the path associated with the incoming request. If you don't want to handle a particular path, you can call ctx.dispatcher.dispatch() to pass it on as normal:

@ApiInterceptor
suspend fun interceptRequest(ctx: ApiInterceptorContext): Response {
    return when {
        ctx.path == "/example" -> Response().apply { body = bodyOf("Intercepted!") }
        // Default: pass request to the route it is normally handled by
        else -> ctx.dispatcher.dispatch()
    }
}

The ApiInterceptorContext class provides access to a mutable version of the incoming request, which gives you a chance to modify it first (e.g. adding cookies, updating headers) before it gets handled, which can be useful.

The ctx.dispatcher.dispatch method takes an optional path you can specify so that you can delegate a request to a different API route:

@ApiInterceptor
suspend fun interceptRequest(ctx: ApiInterceptorContext): Response {
    return when {
        // User will think "/legacy" handled the request;
        // actually, "/new" did
        ctx.path == "/legacy" -> ctx.dispatcher.dispatch("/new")
        else -> ctx.dispatcher.dispatch()
    }
}

Perhaps you aren't interested in interfering with any incoming requests, but you want to modify all responses before they get sent back to the client. You can use this pattern for that:

@ApiInterceptor
suspend fun interceptResponse(ctx: ApiInterceptorContext): Response {
    return ctx.dispatcher.dispatch().also { res ->
        res.headers["X-Intercepted"] = "true"
    }
}
Caution

API interceptors only work for API routes, not static files or other resources. In other words, although you can use an interceptor to intercept a call to "/api/users/edit", the feature is not designed to handle a user navigating to https://example.com/admin/dashboard and then getting redirected to https://example.com/login instead.

API interceptors also do not work for API streams (which will be discussed later in this article) since there's nothing general to intercept there. API streams connect once and then persist.

Dynamic API routes

Similar to Dynamic routes on the frontend, you can define API routes using curly braces in the same way to indicate a dynamic value that should be captured with some binding name.

For example, the following endpoint will capture the value "123" into a key name called "article" when querying articles/123:

@Api("{}")
suspend fun fetchArticle(ctx: ApiContext) {
    val articleId = ctx.req.params["article"] ?: return
    // ...
}
jvmMain/kotlin/com/mysite/api/articles/Article.kt

Recall from the @Page docs that specifying a name inside the curly braces defines the variable name used to capture the value. When empty, as above, Kobweb uses the filename to generate it. In other words, you could explicitly specify @Api("{article}") in the above example for the exact same effect.

Once this API endpoint is defined, query it as you would any normal API endpoint:

coroutineScope.launch {
  // Will cause the "article" variable on the server
 // to get set to "123"
  val articleText = window.api.get("articles/123")
      .bodyAsBytes()
      .decodeToString()
  // ...
}
jsMain/kotlin/com/mysite/pages/articles/Article.kt

@InitApi methods and initializing services

A Kobweb server supports declaring methods that should be run when it starts up. These methods must be annotated with @InitApi and must take a single InitApiContext parameter.

Important

If you are running a development server and change any of your backend code, causing a live reloading event, the init methods will be run again.

The InitApiContext class exposes a mutable set property (called data) which you can put anything into. Meanwhile, @Api methods expose an immutable version of data. This allows you to initialize a service in an @InitApi method and then access it in your @Api methods.

Let's demonstrate a concrete example. Imagine you have an interface called Database and a mutable subclass MutableDatabase that implements it and provides additional APIs for mutating the database.

The skeleton for registering and later querying such a database instance might look like this:

@InitApi
fun initDatabase(ctx: InitApiContext) {
  val db = MutableDatabase()
  db.createTable("users", listOf("id", "name")).apply {
    addRow(listOf("1", "Alice"))
    addRow(listOf("2", "Bob"))
  }
  db.loadResource("products.csv")

  ctx.data.add<Database>(db)
}

@Api
fun getUsers(ctx: ApiContext) {
  if (ctx.req.method != HttpMethod.GET) return
  val db = ctx.data.getValue<Database>()
  ctx.res.body = bodyOf(db.query("SELECT * FROM users").toString())
}

Define API streams

Kobweb servers support persistent connections via streams. Streams are essentially named channels that maintain continuous contact between the client and the server, allowing either side to send messages to the other at any time. This is especially useful if you want your server to be able to communicate updates to your client without needing to poll.

Additionally, multiple clients can connect to the same stream. In this case, the server can choose to not only send a message back to your client, but also to broadcast messages to all users (or a filtered subset of users) on the same stream. You could use this, for example, to implement a chat server with rooms.

Example API stream

Like API routes, API streams must be defined under the api package in your jvmMain source directory. By default, the name of the stream will be derived from the file name and path that it is declared in (e.g. api/lobby/Chat.kt will create a stream named "lobby/chat").

Unlike API routes, API streams are defined as properties, not methods. This is because API streams need to be a bit more flexible than routes, since streams consist of multiple distinct events: client connection, client messages, and client disconnection.

Also unlike API routes, streams do not have to be annotated. The Kobweb Application plugin can automatically detect them.

For example, here's a simple stream, declared on the backend, that echoes back any argument it receives:

val echo = object : ApiStream {
  override suspend fun onClientConnected(ctx: ClientConnectedContext) {
    // Optional: ctx.stream.broadcast a message to all other clients that a new stream connected
    // Optional: Update ctx.data here, initializing data associated with ctx.stream.id
  }
  override suspend fun onTextReceived(ctx: TextReceivedContext) {
    ctx.stream.send(ctx.text)
  }
  override suspend fun onClientDisconnected(ctx: ClientDisconnectedContext) {
    // Optional: ctx.stream.broadcast a message to all other clients that a stream disconnected
    // Optional: Update ctx.data here, removing data associated with ctx.stream.id
  }
}
jvmMain/kotlin/com/mysite/api/Echo.kt

To communicate with an API stream from your site, you need to create a stream connection on the client. We provide the rememberApiStream method to help with this:

@Page
@Composable
fun ApiStreamDemoPage() {
  val echoStream = rememberApiStream("echo", object : ApiStreamListener {
    override fun onConnected(ctx: ConnectedContext) {}
    override fun onTextReceived(ctx: TextReceivedContext) {
      console.log("Echoed: ${ctx.text}")
    }
    override fun onDisconnected(ctx: DisconnectedContext) {}
  })

  Button(onClick = {
    echoStream.send("hello!")
  }) { Text("Click me") }
}
jsMain/kotlin/com/mysite/pages/ApiStreamDemo.kt

After running your project, you can click on the button and check the console logs. If everything is working properly, you should see "Echoed: hello!" each time you press the button.

Tip

Run kobweb create examples/chat to instantiate a project that uses API streams to implement a very simple chat application. Feel free to reference that project for a more realistic example.

API stream conveniences

The above example was intentionally verbose, to showcase the broader functionality around API streams. However, depending on your use-case, you can elide a fair bit of boilerplate.

First of all, the connect and disconnect handlers are optional, so you can omit them if you don't need them. Let's simplify the echo example:

val echo = object : ApiStream {
  override suspend fun onTextReceived(ctx: TextReceivedContext) {
    ctx.stream.send(ctx.text)
  }
}
Backend
val echoStream = rememberApiStream("echo", object : ApiStreamListener {
  override fun onTextReceived(ctx: TextReceivedContext) {
    console.log("Echoed: ${ctx.text}")
  }
})
Frontend

Additionally, if you only care about the text event, there are convenience methods for that:

val echo = ApiStream { ctx -> ctx.stream.send(ctx.text) }
Backend
val echoStream = rememberApiStream("echo") {
  ctx -> console.log("Echoed: ${ctx.text}")
}
Frontend

In practice, your API streams will probably be a bit more involved than the echo example above, but it is nice to know that you can handle some cases only needing a one-liner on the server and another on the client to create a persistent client-server connection!

Note

If you need to create an API stream with stricter control around when it actually connects to the server, you can create the ApiStream object directly instead of using rememberApiStream:

val echoStream = remember { ApiStream("echo") }
val scope = rememberCoroutineScope()

// Later, perhaps after a button is clicked...
scope.launch {
  echoStream.connect(object : ApiStreamListener { /* ... */ })
}

API routes vs. API streams

When faced with a choice, use API routes as often as you can. They are conceptually simpler, and you can query API endpoints with a CLI program like curl and sometimes even visit the URL directly in your browser. They are great for handling queries of or updates to server resources in response to user-driven actions (like visiting a page or clicking on a button). Every operation you perform returns a clear response code in addition to some payload information.

Meanwhile, API streams are very flexible and can be a natural choice to handle high-frequency communication. But they are also more complex. Unlike a simple request / response pattern, you are instead opting in to manage a potentially long lifetime during which you can receive any number of events. You may have to concern yourself about interactions between all the clients on the stream as well. API streams are fundamentally stateful.

You often need to make a lot of decisions when using API streams. What should you do if a client or server disconnects earlier than expected? How do you want to communicate to the client that their last action succeeded or failed (and you need to be clear about exactly which action because they might have sent another one in the meantime)? What structure do you want to enforce, if any, between a client and server connection where both sides can send messages to each other at any time?

Most importantly, API streams may not horizontally scale as well as API routes. At some point, you may find yourself in a situation where a new web server is spun up to handle some intense load.

If you're using API routes, you're already probably delegating to a database service as your data backend, so this may just work seamlessly.

But for API streams, you many naturally find yourself writing a bunch of broadcasting code. However, this only works to communicate between all clients that are connected to the same server. Two clients connected to the same stream on different servers are effectively in different, disconnected worlds.

The above situation is often handled by using a pubsub service (like Redis). This feels somewhat equivalent to using a database as a service in the API route situation, but this code might not be as straightforward to migrate.

API routes and API streams are not a you-must-use-one-or-the-other situation. Your project can use both! In general, try to imagine the case where a new server might get spun up, and design your code to handle that situation gracefully. API routes are generally safe to use, so use them often.

However, if you have a situation where you need to communicate events in real-time, especially situations where you want your client to be continuously directed what to do by the server via events, API streams are a great choice.

Note

You can also search online about REST vs WebSockets, as these are the technologies that API routes and API streams are implemented with. Any discussions about them should apply here as well.

Server logs

When you run kobweb run, the spun-up web server will, by default, log to the .kobweb/server/logs directory.

Note

You can generate logs using the ctx.logger property provided to @Api calls.

You can configure logging behavior by editing the .kobweb/conf.yaml file. Below we show setting all parameters to their default values:

server:
  logging:
    level: DEBUG # ALL, TRACE, DEBUG, INFO, WARN, ERROR, OFF
    enableConsoleLogging: true # If false, logs will not be written to stdout/stderr
    enableFileLogging: true # If false, a log file will not be created
    interceptSystemOutput: true # If true, println and System.err will generate logs
    logRoot: ".kobweb/server/logs"
    clearLogsOnStart: true # Warning - if true, wipes ALL files in logRoot, so don't put other files in there!
    logFileBaseName: "kobweb-server" # e.g. "kobweb-server.log", "kobweb-server.2023-04-13.log"
    maxFileCount: null # null = unbound. One log file is created per day, so 30 = 1 month of logs
    totalSizeCap: 10MiB # null = unbound. Accepted units: B, K, M, G, KB, MB, GB, KiB, MiB, GiB
    compressHistory: true # If true, old log files are compressed with gzip
.kobweb/conf.yaml

The above defaults were chosen to be reasonable for most users running their projects on their local machines in developer mode. However, for production servers, you may want to set clearLogsOnStart to false, bump up the totalSizeCap after reviewing the disk limitations of your web server host, and maybe set maxFileCount to a reasonable limit.

Note

Most users might assume "10MB" is 10 * 1024 * 1024 bytes, but here it will actually result in 10 * 1000 * 1000 bytes. You probably want to use "KiB", "MiB", or "GiB" when you configure this value.

CORS

CORS, or Cross-Origin Resource Sharing, is a security feature built on the idea that a web page should not be able to make requests for resources from a server that is not the same as the one that served the page unless it was served from a trusted domain.

To configure CORS for a Kobweb backend, Kobweb's .kobweb/conf.yaml file allows you to declare such trusted domains using a cors block:

server:
  cors:
    hosts:
      - name: "example.com"
        schemes:
          - "https"
.kobweb/conf.yaml
Note

Specifying the schemes is optional. If you don't specify them, Kobweb defaults to "http" and "https".

Note

You can also specify subdomains, e.g.

- name: "example.com"
  subdomains:
    - "en"
    - "de"
    - "es"

which would add CORS support for en.example.com, de.example.com, and es.example.com, as well as example.com itself.

Once configured, your Kobweb server will be able to respond to data requests from any of the specified hosts.

Tip

If you find that your full-stack site, which was working locally during development, rejects requests in the production version, check your browser's console logs. If you see errors in there about a violated CORS policy, that means you didn't configure CORS correctly.

Passing in system properties

You may find yourself wanting to pass a value from your build script into your server code. To do this, you can add values into the kobweb.app.server.systemProperties property.

kobweb {
    app {
        server {
            systemProperties.put("db.type", "MongoDb")
        }
    }
}
site/build.gradle.kts

and then, just call System.getProperty in your backend code:

enum class DbType {
    InMemory,
    MongoDb
}

interface Database {
   interface Result { /* ... */ }
   fun query(input: String): Result
}

@InitApi
fun initDatabase(ctx: InitApiContext) {
   val dbType = SystemProperty.get("db.type").let { DbType.valueOf(it) }
   val db = when (dbType) {
       DbType.InMemory -> InMemoryDatabase()
       DbType.MongoDb -> MongoDatabase()
   }
   ctx.data.add<Database>(db)
}
site/src/jvmMain/kotlin/model/Database.kt

A useful pattern could be to allow users to override the value via an environment variable, at which point you could control it from, for example, your CI/CD runner:

kobweb {
    app {
        server {
            systemProperties.put(
               "db.type",
               providers.environmentVariable("DB_TYPE")
                  .orElse("InMemory")
                  .get()
            )
        }
    }
}
site/build.gradle.kts
# ...

env:
  DB_TYPE: MongoDb

# ...
.github/workflows/export-prod.yaml
This site is open source.
Copyright © 2026 Varabyte. All rights reserved.