Just discovered that it is possible to write edge ...
# supabase-kt
m
Just discovered that it is possible to write edge functions using kotlin and supabase-kt instead of typescript and supabase-js 👍
K 1
j
Via Kotlin/JS?
m
Yes
According to my observations, prerequisites are: • Kotlin 2.0 (which bring ES modules support) • JS IR with ES modules enabled (otherwise imports will not work as expected) For now, to make it works I also have to set: • Whole project JS IR output granularity (because of the way imports are done in generated kotlin code) but this can be fixed. • NodeJs inside JS IR target And for things to get simpler, I've created two gradle tasks (same task implementation) for development and production that depends respectively on jsNodeDevelopmentLibraryDistribution and jsNodeProductionLibraryDistribution tasks. The second one also invoke the supabase function deploy command. The task generate two files: • index.ts: contains the Deno serve function that will call and wait for the kotlin function to finish. • import_map.json: contains all the dependencies from the package.json previously generated by the dependent kotlin task. This is necessary because of the way Deno works with imports even through Deno supports package.json. For now this integration was experimental and can be greatly improved but it is sufficient to confirm that it is possible to write edge function using kotlin.
Below an exemple of a hello function written in kotlin: index.ts (generated):
Copy code
import { hello } from './build/dist/js/productionLibrary/hello.mjs';

Deno.serve(async (req) => {
   return await hello(req)
})
import_map.json (generated):
Copy code
{
  "imports": {
    "format-util": "npm:format-util@^1.0.5",
    "node-fetch": "npm:node-fetch@2.6.7",
    "abort-controller": "npm:abort-controller@3.0.0",
    "ws": "npm:ws@8.5.0",
    "@js-joda/core": "npm:@js-joda/core@3.2.0"
  }
}
hello.kt (note that the body is suspending):
Copy code
@Serializable
data class RequestBody(
    val name: String
)

@Serializable
data class ResponseBody(
    val message: String?
)

@OptIn(ExperimentalJsExport::class)
@JsExport
fun hello(request: Request) = serve {
    val requestBody = request.body<RequestBody>()

    val client = io.github.jan.supabase.createSupabaseClient(
        supabaseUrl = Deno.env[SupabaseSecrets.URL]!!,
        supabaseKey = Deno.env[SupabaseSecrets.ANON_KEY]!!
    ) {
        install(Auth) {
            codeVerifierCache = MemoryCodeVerifierCache()
            sessionManager = MemorySessionManager()
        }
    }

    jsonResponse(
        body = ResponseBody(
            message = "Hello ${requestBody.name} from ${client.supabaseUrl}",
        )
    )
}
serve.kt (from a shared module between functions, using composite build):
Copy code
@OptIn(DelicateCoroutinesApi::class)
fun serve(action: suspend CoroutineScope.() -> Response): Promise<Response> {
    return GlobalScope
        .async(
            context = Dispatchers.Unconfined,
            start = CoroutineStart.UNDISPATCHED,
            block = action
        )
        .asPromise()
}
other things used (from different files in the same module as the serve function):
Copy code
external interface Env {
    operator fun get(key: String): String?
}

external object Deno {
    val env: Env
}
Copy code
object SupabaseSecrets {
    const val URL = "SUPABASE_URL"
    const val ANON_KEY = "SUPABASE_ANON_KEY"
    const val SERVICE_ROLE_KEY = "SUPABASE_SERVICE_ROLE_KEY"
    const val DB_URL = "SUPABASE_DB_URL"
}
Copy code
// Gets the request body as instance of type T
suspend inline fun <reified T : Any> Request.body(): T {
    return Json.decodeFromString<T>(text().await())
}
Copy code
inline fun <reified T : Any> jsonResponse(
    body: T,
    init: ResponseInit = ResponseInit(
        headers = Headers {
            jsonContentType()
        }
    )
): Response = Response(
    body = Json.encodeToString(body),
    init = init
)
Conjugated with gradle continuous build and gradle configuration cache, the development experience is more than satisfactory. It requires less than 5 seconds in my machine (16" 2019 MBP) to compile that hello function and changing output granularity could improve things. During development, edge function invocation takes ≈400ms but once deployed in production it takes <100ms.
j
Nice work! We could transform this into a small experimental template/sample and steadily improve it. I'm sure a lot of people using Kotlin would prefer using Kotlin all the way.
🙏 1
plus1 1
m
I think so too. I’ll continue to play with that to find the limits and try to provide an experimental runtime lib accompanied by a gradle plugin.
👍 2
t
I also write edge functions in kt 👍 I am using FCM but couldn't find a way to get a FCM access token in pure Kotlin and had to use js. If you also need to use your own js files : in Kotlin :
Copy code
@file:JsModule("./my-file.js")


import kotlin.js.Promise

@JsName("getFCMAccessToken")
external fun getFCMAccessToken(): Promise<String>
in src/jsMain/resources/my-file.js
Copy code
import { JWT } from 'npm:google-auth-library@9'
import serviceAccount from '../service-account.json' with { type: 'json' }

//<https://supabase.com/docs/guides/functions/examples/push-notifications?queryGroups=platform&platform=fcm>

export function getFCMAccessToken() {
  return new Promise((resolve, reject) => {
    const jwtClient = new JWT({
      email: serviceAccount.client_email,
      key: serviceAccount.private_key,
      scopes: ['<https://www.googleapis.com/auth/firebase.messaging>'],
    })
    jwtClient.authorize((err, tokens) => {
      if (err) {
        reject(err)
        return
      }
      resolve(tokens.access_token)
    })
  })
}
And that way you can use it from Kotlin.
I am currently trying to access postgres database directly, without using supabase.postgrest to execute multiple queries inside a transaction
m
Have you tried https://github.com/GitLiveApp/firebase-kotlin-sdk for getting an FCM access token in pure Kotlin ? (never tried it myself)
t
I'll take a look, thanks
b
@Maanrifa Bacar Ali I’m trying to follow your tips for creating my own edge functions with Kotlin. How did you get that index.ts with the generated Deno? What gradle task are you running to build How do you test it locally? (localhost:8080?) I’m just experimenting with it now but it would be really cool to have the edge function
m
Forget these tips, I’m working on something better, you’ll only have to focus on your business code. I plan to release something (experimental) by the end of the week or in the next 5 days
👏 2
b
Very cool. Thanks for the contribution. I don’t know anything about JS, but I know Kotlin very well so I’m trying to keep everything in house with my Kotlin Multiplatform app
K 1
Any update to this? It would be cool to use Kotlin for an edge function.
m
Still on it, I was a little busy at work this week so I didn’t progress as planned. It’s about to be finished. I am currently working on composite build / projet dependency and js interoprability for sharing code between multiple projects with js sources. I ended up adding features that weren’t initially planned but that’s because I need them for my business. That’s why it took a little longer than expected.
🙏🏼 1
❤️ 1
I’ve finished writing source code and created an example repository. I’m currently writing the readme, generating documentation and dealing with maven for release. It will be ready for tomorrow and I’ll post the repository link here once available.
b
Sweet thank you. Long live Kotlin
K 1
m
b
Wow, this is much more than I was expecting. Great job! Thanks for your hard work.
K 1
m
It would be appreciated if someone could just confirm that they managed to compile the example, when they have time
t
Did you have any luck with user auth for activating RLS policies ?
In JS, it's possible to create the client like so :
Copy code
const supabaseClient = createClient(
  Deno.env.get('SUPABASE_URL') ?? '',
  Deno.env.get('SUPABASE_ANON_KEY') ?? '',
  // Create client with Auth context of the user that called the function.
  // This way your row-level-security (RLS) policies are applied.
  {
    global: {
      headers: { Authorization: req.headers.get('Authorization')! },
    },
  }
)
Is there a way to do the same in kt ?
@Jan maybe an idea ?
j
You can either just import the token into the Auth plugin or set the token per plugin. 1:
Copy code
supabaseClient.auth.importAuthToken(tokenFromHeader)
2:
Copy code
install(Postgrest) {
   jwtToken = tokenFromHeader
}
The 1. approach will assure that all other plugins use this token. Also, you should probably always use
AuthConfig#minimalSettings
when working with server-side code to avoid session saving somewhere you don't want
For creating a client, you can just have a look at the example
t
thanks a lot for the detailed and quick answer !
supabaseClient.auth.importAuthToken
works like a charm ❤️
kodee loving 2
110 Views