<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Work In Progress]]></title><description><![CDATA[I'm a software architect, engineer, and infrastructure passionate about building internal tools. Currently, I'm trying several Platform as a Services, and build]]></description><link>https://blog.tegar.my.id</link><generator>RSS for Node</generator><lastBuildDate>Tue, 14 Apr 2026 10:19:21 GMT</lastBuildDate><atom:link href="https://blog.tegar.my.id/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Introducing Scripts: Securely Store and Reuse Code From Internet.]]></title><description><![CDATA[How many times you write a bash script, python, config file, Dockerfile, gitignore and other kind of script or configuration but ended up forgot where you use it or how you use it? Of course you can create it again, with help of AI and generator, but...]]></description><link>https://blog.tegar.my.id/introducing-scripts-securely-store-and-reuse-code-from-internet</link><guid isPermaLink="true">https://blog.tegar.my.id/introducing-scripts-securely-store-and-reuse-code-from-internet</guid><category><![CDATA[ModusHack]]></category><dc:creator><![CDATA[Tegar Imansyah]]></dc:creator><pubDate>Sat, 30 Nov 2024 15:51:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732981165239/a53dd4c9-9766-492c-88ff-3148b845291a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>How many times you write a bash script, python, config file, Dockerfile, gitignore and other kind of script or configuration but ended up forgot where you use it or how you use it? Of course you can create it again, with help of AI and generator, but most of the time you need to tweak it again to your suitable use case. Well, we've been in that position, often.</p>
<p>So that’s why we create <strong>Scripts by Implementing Cloud</strong> 🎉. It’s like a pastebin or gist in steroid. Scripts not only save your script, but you can directly download or run using simple <code>curl</code> command. Not only that, Scripts will scan all the code uploaded to Scripts and give you a notes for best practice and security level, thanks to AI checker. We even give you warning or prevent you to run a public script that may dangerously make your computer havoc, steal your data or connect to suspicious server. No more worry about using code from internet. See the demo below and start storing your script in Scripts.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732980728261/089cc3f7-ee98-4b30-8482-a0f75c6dc41f.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Scripts is a one of many dev tools that we build under <a target="_blank" href="https://implementing.cloud"><strong>Implementing.Cloud</strong></a> umbrella. While the main project is still under development, we proud that we finish the development of Scripts that will heavily used in the main project. Scripts also participate with Hashnode &gt;&lt; Hypermode in ModusHack Hackathon.</p>
</blockquote>
<p>[Placeholder for demo video]</p>
<p>When we say Scripts will securing your script, don't take my word for it; look the <a target="_blank" href="https://github.com/tegarimansyah/implementing-cloud-scripts-modus">code in Github</a>. You can easily reproduce it by using my nix configuration. Simply run <code>nix develop</code> and you got all the requirements you need.</p>
<p>And while we was working on it, we create a <a target="_blank" href="https://github.com/hypermodeinc/docs/pull/69">PR for the use of JWKS documentation</a>.</p>
<h2 id="heading-initial-system-design-iteration-and-exploration">Initial System Design, Iteration and Exploration</h2>
<p>We want our user interact with Scripts in two way:</p>
<ol>
<li><p>Submit and browse their code via web app</p>
</li>
<li><p>Get or execute their code via terminal</p>
</li>
</ol>
<p>To make sure there is no abuse to platform, only authenticated user can submit their code but everyone can download it. The submission is not a simple insert to database, but there will be some AI analysis to determine whether it follow best practice and most importantly is it secure or not to execute. We also wrap the original code with our predefined script to restrict the execution and give user some warning based on the analysis and community notes before the execution.</p>
<p>Actually we are really interested to utilize an automatic sandboxing in Modus by executing the script in the platform, but we don't have much time to explore and debug it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732978262245/f979b28d-2c7d-4874-b7b9-05c1da9b5c1c.png" alt class="image--center mx-auto" /></p>
<p>After some time exploring modus, we learn several things (and detail about it in the next few sections):</p>
<ol>
<li><p>We are using Go and it’s incredibly easy to make a function available for request</p>
</li>
<li><p>Currently modus only support GraphQL. I hope in the future I can see support for RESTful API (Achievable with API Gateway) and async request for background process, especially AI process without blocking user request (Achievable with embedding NATS outside modus).</p>
</li>
<li><p>It support Auth via JWT out of the box, but we have difficulties to mix private and public space</p>
</li>
<li><p>Because the sandboxing nature, we need to “whitelist” to external server. I use this to connect my database via <a target="_blank" href="https://postgrest.org/">PostgREST</a>, connecting to PostgreSQL via RESTful API.</p>
</li>
<li><p>[Placeholder to talk about AI]</p>
</li>
</ol>
<h3 id="heading-comparing-syntax-for-modus-with-faas">Comparing Syntax for Modus with FaaS</h3>
<p>Let’s assume that we will have a function that receive a owner of the script and the name of the script and will return the Code struct like this. It will call a database but let’s leave it for later discussion.</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> Code <span class="hljs-keyword">struct</span> {
    Owner         <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"owner"`</span>
    Name          <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"name"`</span>
    CodeType      <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"code_type"`</span>
    ScriptContent <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"script_content"`</span>
}
</code></pre>
<p>First of all, let's see the contender</p>
<h4 id="heading-aws-lambda"><strong>AWS Lambda</strong></h4>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"encoding/json"</span>
    <span class="hljs-string">"github.com/aws/aws-lambda-go/events"</span>
    <span class="hljs-string">"github.com/aws/aws-lambda-go/lambda"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GetCode</span><span class="hljs-params">(owner, name <span class="hljs-keyword">string</span>)</span> *<span class="hljs-title">Code</span></span> {
    code := db.GetCode(owner, name)
    <span class="hljs-keyword">return</span> &amp;Code{
        Owner:         owner,
        Name:          name,
        CodeType:      code.Type,
        ScriptContent: code.Content,
    }
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">handler</span><span class="hljs-params">(ctx context.Context, request events.APIGatewayProxyRequest)</span> <span class="hljs-params">(events.APIGatewayProxyResponse, error)</span></span> {
    owner := request.QueryStringParameters[<span class="hljs-string">"owner"</span>]
    name := request.QueryStringParameters[<span class="hljs-string">"name"</span>]

    code := GetCode(owner, name)

    responseJSON, _ := json.Marshal(code)
    <span class="hljs-keyword">return</span> events.APIGatewayProxyResponse{
        StatusCode: <span class="hljs-number">200</span>,
        Body:       <span class="hljs-keyword">string</span>(responseJSON),
    }, <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    lambda.Start(handler)
}
</code></pre>
<h4 id="heading-gcp-functions"><strong>GCP Functions</strong></h4>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> function

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"encoding/json"</span>
    <span class="hljs-string">"net/http"</span>
)

<span class="hljs-keyword">type</span> Code <span class="hljs-keyword">struct</span> {
    Owner         <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"owner"`</span>
    Name          <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"name"`</span>
    CodeType      <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"code_type"`</span>
    ScriptContent <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"script_content"`</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GetCode</span><span class="hljs-params">(owner, name <span class="hljs-keyword">string</span>)</span> *<span class="hljs-title">Code</span></span> {
    code := db.GetCode(owner, name)
    <span class="hljs-keyword">return</span> &amp;Code{
        Owner:         owner,
        Name:          name,
        CodeType:      code.Type,
        ScriptContent: code.Content,
    }
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">CodeHandler</span><span class="hljs-params">(w http.ResponseWriter, r *http.Request)</span></span> {
    owner := r.URL.Query().Get(<span class="hljs-string">"owner"</span>)
    name := r.URL.Query().Get(<span class="hljs-string">"name"</span>)

    code := GetCode(owner, name)

    w.Header().Set(<span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"application/json"</span>)
    json.NewEncoder(w).Encode(code)
}
</code></pre>
<h4 id="heading-azure-functions"><strong>Azure Functions</strong></h4>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"encoding/json"</span>
    <span class="hljs-string">"net/http"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GetCode</span><span class="hljs-params">(owner, name <span class="hljs-keyword">string</span>)</span> *<span class="hljs-title">Code</span></span> {
    code := db.GetCode(owner, name)
    <span class="hljs-keyword">return</span> &amp;Code{
        Owner:         owner,
        Name:          name,
        CodeType:      code.Type,
        ScriptContent: code.Content,
    }
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">CodeHandler</span><span class="hljs-params">(w http.ResponseWriter, r *http.Request)</span></span> {
    owner := r.URL.Query().Get(<span class="hljs-string">"owner"</span>)
    name := r.URL.Query().Get(<span class="hljs-string">"name"</span>)

    code := GetCode(owner, name)

    w.Header().Set(<span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"application/json"</span>)
    json.NewEncoder(w).Encode(code)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    http.HandleFunc(<span class="hljs-string">"/api/GetCode"</span>, CodeHandler)
    http.ListenAndServe(<span class="hljs-string">":8080"</span>, <span class="hljs-literal">nil</span>)
}
</code></pre>
<h4 id="heading-cloudflare-worker"><strong>Cloudflare Worker</strong></h4>
<p>Worker known for Javascript runtime, but like Modus, Cloudflare Worker also available for Go by compile it to WASM.</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"encoding/json"</span>
    <span class="hljs-string">"syscall/js"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GetCode</span><span class="hljs-params">(owner, name <span class="hljs-keyword">string</span>)</span> *<span class="hljs-title">Code</span></span> {
    code := db.GetCode(owner, name)
    <span class="hljs-keyword">return</span> &amp;Code{
        Owner:         owner,
        Name:          name,
        CodeType:      code.Type,
        ScriptContent: code.Content,
    }
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">handleRequest</span><span class="hljs-params">(this js.Value, args []js.Value)</span> <span class="hljs-title">interface</span></span>{} {
    query := args[<span class="hljs-number">0</span>]
    owner := query.Get(<span class="hljs-string">"owner"</span>).String()
    name := query.Get(<span class="hljs-string">"name"</span>).String()

    code := GetCode(owner, name)
    codeJSON, _ := json.Marshal(code)

    <span class="hljs-keyword">return</span> js.ValueOf(<span class="hljs-keyword">string</span>(codeJSON))
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    js.Global().Set(<span class="hljs-string">"handleRequest"</span>, js.FuncOf(handleRequest))
    <span class="hljs-keyword">select</span> {}
}
</code></pre>
<h4 id="heading-openfaas"><strong>OpenFaas</strong></h4>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> function

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"encoding/json"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"net/http"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GetCode</span><span class="hljs-params">(owner, name <span class="hljs-keyword">string</span>)</span> *<span class="hljs-title">Code</span></span> {
    code := db.GetCode(owner, name)
    <span class="hljs-keyword">return</span> &amp;Code{
        Owner:         owner,
        Name:          name,
        CodeType:      code.Type,
        ScriptContent: code.Content,
    }
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Handle</span><span class="hljs-params">(w http.ResponseWriter, r *http.Request)</span></span> {
    <span class="hljs-comment">// Parse query parameters</span>
    owner := r.URL.Query().Get(<span class="hljs-string">"owner"</span>)
    name := r.URL.Query().Get(<span class="hljs-string">"name"</span>)

    <span class="hljs-keyword">if</span> owner == <span class="hljs-string">""</span> || name == <span class="hljs-string">""</span> {
        http.Error(w, <span class="hljs-string">"Missing owner or name query parameter"</span>, http.StatusBadRequest)
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-comment">// Get the code details</span>
    code := GetCode(owner, name)

    <span class="hljs-comment">// Respond with JSON</span>
    w.Header().Set(<span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"application/json"</span>)
    <span class="hljs-keyword">if</span> err := json.NewEncoder(w).Encode(code); err != <span class="hljs-literal">nil</span> {
        http.Error(w, <span class="hljs-string">"Error encoding response"</span>, http.StatusInternalServerError)
        <span class="hljs-keyword">return</span>
    }
}
</code></pre>
<h4 id="heading-knative-functions"><strong>Knative Functions</strong></h4>
<p>Open source solution that utilize Knative that work on top of Kubernetes.</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"encoding/json"</span>
    <span class="hljs-string">"net/http"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GetCode</span><span class="hljs-params">(owner, name <span class="hljs-keyword">string</span>)</span> *<span class="hljs-title">Code</span></span> {
    code := db.GetCode(owner, name)
    <span class="hljs-keyword">return</span> &amp;Code{
        Owner:         owner,
        Name:          name,
        CodeType:      code.Type,
        ScriptContent: code.Content,
    }
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">CodeHandler</span><span class="hljs-params">(w http.ResponseWriter, r *http.Request)</span></span> {
    owner := r.URL.Query().Get(<span class="hljs-string">"owner"</span>)
    name := r.URL.Query().Get(<span class="hljs-string">"name"</span>)

    code := GetCode(owner, name)

    w.Header().Set(<span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"application/json"</span>)
    json.NewEncoder(w).Encode(code)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    http.HandleFunc(<span class="hljs-string">"/"</span>, CodeHandler)
    http.ListenAndServe(<span class="hljs-string">":8080"</span>, <span class="hljs-literal">nil</span>)
}
</code></pre>
<h4 id="heading-hypermode-modus"><strong>Hypermode Modus</strong></h4>
<p>With Modus, we remove all the boilerplate of parsing request and response. The true function as a service.</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GetCode</span><span class="hljs-params">(owner, name <span class="hljs-keyword">string</span>)</span> *<span class="hljs-title">Code</span></span> {
    code := db.GetCode(owner, name)
    <span class="hljs-keyword">return</span> &amp;Code{
        Owner:    owner,
        Name:     name,
        CodeType: code.Type,
        ScriptContent:   code.Content,
    }
}
</code></pre>
<p>As you can see, it’s incredibly easy to write Modus compared to other solutions. Especially, it already parse the request into our defined struct without manual binding.</p>
<h3 id="heading-exploring-modus-sdk-and-runtime">Exploring Modus SDK and Runtime</h3>
<p>I have quite a long discussion and reporting a bug in <a target="_blank" href="https://discord.com/channels/1267579648657850441/1311173641190510674">Hypermode discord</a>. It’s fun to learn how the modus work, thanks to the beauty of open source. First we can see, beside <code>modus dev</code>, there is another things like “runtime” and “sdk”. We can use different version of runtime and SDK by installing it by using <code>modus runtime install v0.14.3</code> or <code>modus sdk install go v0.14.3</code>, see the source of release in <a target="_blank" href="https://github.com/hypermodeinc/modus/releases/">github release</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732964829986/ad7ac27a-0a5d-4eda-8dae-70e2edf6647e.png" alt class="image--center mx-auto" /></p>
<p>After it install, we can use the specific runtime by running <code>modus dev -r v0.13.1</code>. For SDK, add the version we want to use in the <code>go.mod</code></p>
<pre><code class="lang-go">module infinite-script

<span class="hljs-keyword">go</span> <span class="hljs-number">1.23</span><span class="hljs-number">.3</span>

require github.com/hypermodeinc/modus/sdk/<span class="hljs-keyword">go</span> v0<span class="hljs-number">.14</span><span class="hljs-number">.3</span> <span class="hljs-comment">// indirect</span>
</code></pre>
<p>There are a “hidden” tool that generate additional go code <code>modus_pre_generated</code> and <code>modus_post_generated.go</code>, it’s called <code>modus-go-build</code> that inside the <code>.modus/sdk/go/&lt;version&gt;</code> folder. Sometimes, it doesn’t install along with the <code>modus sdk install</code> command, so as a workaround I run <code>GOBIN=~/.modus/sdk/go/v0.14.3/ go install github.com/hypermodeinc/modus/sdk/go/tools/modus-go-build@v0.14.3</code> manually. Well, I found the command after looking at the source code.</p>
<p><img src="https://media.discordapp.net/attachments/1311173641190510674/1311509618161225799/image.png?ex=674bc103&amp;is=674a6f83&amp;hm=3f99374b1a3df06db2708f7c774246e4a0a5d45603b88bb5b8cf565c9e9868cc&amp;=&amp;format=webp&amp;quality=lossless&amp;width=378&amp;height=610" alt="Image" class="image--center mx-auto" /></p>
<p>All SDK and runtime is saved under <code>$HOME/.modus</code></p>
<h3 id="heading-exploring-graphql-and-auth">Exploring GraphQL and Auth</h3>
<p>This simple <code>modus.json</code> that came from template will generate this nice GraphQL Explorer when running locally.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"https://schema.hypermode.com/modus.json"</span>,
  <span class="hljs-attr">"endpoints"</span>: {
    <span class="hljs-attr">"default"</span>: {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"graphql"</span>,
      <span class="hljs-attr">"path"</span>: <span class="hljs-string">"/graphql"</span>,
      <span class="hljs-attr">"auth"</span>: <span class="hljs-string">"bearer-token"</span>
    }
  }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732963134701/cc802b29-418d-41d3-842b-fedfef7bdbc5.png" alt class="image--center mx-auto" /></p>
<p>As you can see, it has <code>"auth": "bearer-token"</code> but actually it will be still be public as long as we are not set <code>MODUS_PEMS</code> like <a target="_blank" href="https://docs.hypermode.com/modus/authentication">defined in the doc here</a>. But honestly I don’t like how we set it up since we need to put the public key (that has very sensitive even we miss a single newline character) in the JSON and serialize it in a single line of string. On the other hand, I use <a target="_blank" href="https://goauthentik.io/">Authentik</a> for my Auth Provider (Like auth0 or clerk, but self-hosted) and it support providing the public key by serve a JWKS endpoint. Does modus support it? Yes! Unfortunately undocumented.</p>
<p>I found it in the <a target="_blank" href="https://github.com/hypermodeinc/modus/blob/main/CHANGELOG.md">Changelog</a> (what a great changelog!), it introduce at <a target="_blank" href="https://github.com/hypermodeinc/modus/blob/main/CHANGELOG.md#2024-10-25---version-0130-all-components">version v0.13.0</a> with <a target="_blank" href="https://github.com/hypermodeinc/modus/pull/511">this PR</a> and <a target="_blank" href="https://github.com/hypermodeinc/modus/pull/594">a fix</a> in <a target="_blank" href="https://github.com/hypermodeinc/modus/blob/main/CHANGELOG.md#2024-11-23---runtime-0140">version v0.14.0</a>. TLDR, all you need is to add <code>MODUS_JWKS_ENDPOINTS</code> in <code>.env.dev.local</code>. If you deploy your app, don’t forget to add it in the dashboard.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732967129643/28338a39-775f-4a36-a802-394a432b7d63.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-bash">MODUS_JWKS_ENDPOINTS=<span class="hljs-string">'{"implementing.cloud":"https://auth.tegar.my.id/application/o/implementing-cloud/jwks/"}'</span>
</code></pre>
<p>But you don’t need authentik to use JWKS, you can use your favorite auth provider and the popular one most likely have this feature shipped. The interesting part is we can have multiple auth provider at once!</p>
<h3 id="heading-add-postman-for-testing-auth-protected-api">Add Postman for Testing Auth Protected API</h3>
<p>After we put <code>MODUS_JWKS_ENDPOINTS</code> or <code>MODUS_PEMS</code>, the built in GraphQL explorer will not work. It also quite a troublesome to copy-paste JWT token from network to use in our testing, especially if we set the life time of access token is only a few minutes.</p>
<p>Since I use OAuth 2.0 based with browser login, you can use Postman to automatically retrieve the token.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732967900000/8ac046ee-4c9f-474b-a4de-1084be4fb73b.png" alt class="image--center mx-auto" /></p>
<p>If success, you can get the schema and make a request with postman to Modus. Don’t forget to enable <code>Using GraphQL introspection</code> instead of import a GraphQL Schema.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732967969197/eafb1779-1c6d-4ff0-b1ab-f34c43b2e6ed.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-mixing-public-and-private-route">Mixing Public and Private Route</h3>
<p><strong>Attempt #1</strong>: Make every functions behind authentication</p>
<p>This is my initial setup that I research above. But when integrating with frontend, I just realized that not all API should behind the auth, especially when get the script or list the public scripts. I ask in discord but unfortunately <a target="_blank" href="https://discord.com/channels/1267579648657850441/1312047609149526079">it’s not supported yet</a>. They suggest I need to deploy 2 different app, but let’s try some workaround in the next attempt.</p>
<p><strong>Attempt #2</strong>: Create 2 endpoints in <code>modus.json</code></p>
<p>I try to modify the file with this configuration</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"https://schema.hypermode.com/modus.json"</span>,
  <span class="hljs-attr">"endpoints"</span>: {
    <span class="hljs-attr">"private"</span>: {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"graphql"</span>,
      <span class="hljs-attr">"path"</span>: <span class="hljs-string">"/private"</span>,
      <span class="hljs-attr">"auth"</span>: <span class="hljs-string">"bearer-token"</span>
    },
    <span class="hljs-attr">"public"</span>: {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"graphql"</span>,
      <span class="hljs-attr">"path"</span>: <span class="hljs-string">"/public"</span>,
      <span class="hljs-attr">"auth"</span>: <span class="hljs-string">"none"</span>
    }
  }
}
</code></pre>
<p>I try to run it locally and it work. Both endpoints will have the same schema and same target functions. I can add <code>Public</code> and <code>Private</code> in the function name and just reject unauthorized request that going to private.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732975675652/61024bf8-d42d-4a03-97c6-325cc0d17f27.png" alt class="image--center mx-auto" /></p>
<p>Unfortunately, it’s currently not working when I deploy it. It’s looks like they hardcode <code>/graphql</code> path in the dashboard and only open that path, my non <code>/graphql</code> path like <code>/private</code> and <code>/public</code> return 404. Please check the <a target="_blank" href="https://discord.com/channels/1267579648657850441/1312248818535764059">discussion here</a>.</p>
<p><strong>Attempt #3</strong>: Create 2 deployment</p>
<p>Okay actually I want to make this my last attempt. So I try to make a monorepo, so multiple modus app in one app. It will help me shared the same code with ease. Modus github action template is awesome, it also have <code>MODUS_DIR</code> env. But unfortunately it’s not yet supported. Even though I have 2 CI and separate trigger, there is no way to specify this target should listen to which CI or path. So the current solution is implement with 2 projects with 2 different repositories.</p>
<p><strong>Bonus Attempts</strong>: Add KrakenD API Gateway to make a RESTful API Request</p>
<p>GraphQL is offering a great flexibility to query data, so there will be no over-fetching in the client side. But if we publicly make the GraphQL available for everyone without limit, then it will be easily abused by bad actor. It’s usually call <strong>Nested Query Attack</strong>, like discussed in the <a target="_blank" href="https://stackoverflow.com/questions/37337466/how-do-you-prevent-nested-attack-on-graphql-apollo-server">stackoverflow</a>. While I’m not sure Modus will suffer this attack (in my case, I can’t see how it possible) but the solution usually is to implement a persisted queris that looks like this, I’m not the fan of it though.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"variables"</span>: {
    <span class="hljs-attr">"path"</span>: <span class="hljs-string">"/article-path"</span>
  },
  <span class="hljs-attr">"extensions"</span>: {
    <span class="hljs-attr">"persistedQuery"</span>: {
      <span class="hljs-attr">"sha256Hash"</span>: <span class="hljs-string">"c205cf782b5c43c3fc67b5233445b78fbea47b99a0302cf31bda2a8e2162e1e6"</span>
    }
  }
}
</code></pre>
<p>I will add API Gateway in front of them to make it easier to read, and additionally give more features like rate-limit, parallel query, logging, observability and hide our both public and private Modus deployment. I choose KrakenD because I have some experience with it, but every other API Gateway will also works. Honestly I wish Hypermode will provide it in the future out-of-the-box.</p>
<p><img src="https://www.krakend.io/images/documentation/graphql/krakend-graphql.png" alt="GraphQL Backend Integration" /></p>
<p>KrakenD is open source API Gateway that can <a target="_blank" href="https://www.krakend.io/docs/backends/graphql/">connect to GraphQL</a>. The example of the configuration is something like this.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"endpoint"</span>: <span class="hljs-string">"/code/{username}/{scriptname}"</span>,
    <span class="hljs-attr">"method"</span>: <span class="hljs-string">"POST"</span>,
    <span class="hljs-attr">"backend"</span>: [
        {
            <span class="hljs-attr">"timeout"</span>: <span class="hljs-string">"4000ms"</span>,
            <span class="hljs-attr">"url_pattern"</span>: <span class="hljs-string">"/graphql?timeout=4s"</span>,
            <span class="hljs-attr">"extra_config"</span>: {
                <span class="hljs-attr">"backend/graphql"</span>: {
                    <span class="hljs-attr">"type"</span>:  <span class="hljs-string">"mutation"</span>,
                    <span class="hljs-attr">"query_path"</span>: <span class="hljs-string">"./graphql/mutations/code.graphql"</span>,
                    <span class="hljs-attr">"variables"</span>: {
                        <span class="hljs-attr">"user"</span>: <span class="hljs-string">"{username}"</span>,
                        <span class="hljs-attr">"scriptname"</span>: <span class="hljs-string">"{scriptname}"</span>,
                        <span class="hljs-attr">"other_static_variables"</span>: {
                            <span class="hljs-attr">"foo"</span>: <span class="hljs-literal">false</span>,
                            <span class="hljs-attr">"bar"</span>: <span class="hljs-literal">true</span>
                        }
                    },
                    <span class="hljs-attr">"operationName"</span>: <span class="hljs-string">"addMktPreferencesForUser"</span>
                }
            }
        }
    ]
}
</code></pre>
<p><strong>Attempt #4</strong>: Back to #1 =&gt; make every functions behind authentication from API Gateway</p>
<p>Well, everything is a cycle. I just realize this approach after implementing API Gateway. Remember that we can have multiple JWKS and PEM in one app? Yes! Why not make the public route is accessible from public, but API Gateway will supply the authentication. With this implementation, I can remove my second application and all duplication I made.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733024800847/138666b0-2e94-4898-8a66-2ca78ed214ce.png" alt class="image--center mx-auto" /></p>
<p>Above are two requests against same deployed modus app. One is using OAuth 2.0 for User and the second one is using normal JWT created by jwt.io using my own private+public key pair. The JWT I create is using <code>"sub": "service-account"</code> and I arbitrary give the expired time 2099. Just make sure the JWT is not leaked :D.</p>
<h3 id="heading-add-async-request-for-background-process">Add Async Request for Background Process</h3>
<p>Unlike other FaaS, modus doesn't have this feature built-in yet. Async request is great for detach current user request to run long running process in the background. But it doesn't mean we can't implement it with modus. Here is a workaround.</p>
<p>To make a async request, usually we need intermediary service as a broker. In this scenario I'm using nats and embed it in our existing go app to keep the architecture simple.</p>
<p>When functionA in modus finish to validate and save to database, it will send an async request to NATS with the inserted id and response to user. NATS that received the request will make another request to functionB in modus to run long running process: AI analysis. The process will have a 2 steps ideally, first using faster AI model we will evaluate the score of best practice and security risk. If there is a single potential risk, we will re-evaluate it with more advance AI model and ask for more comprehensive reasoning.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733025628951/8ab44c56-94d5-450e-b200-5987ed774f56.png" alt class="image--center mx-auto" /></p>
<p>With this mechanism, we will get both benefit of run an advance model if needed or use smaller and cheaper model in most case without compromised user experience.</p>
<h3 id="heading-final-design-system"><strong>Final Design System</strong></h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733025814378/4efb3165-949b-477f-8933-239310080be7.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-code-implementation">Code Implementation</h2>
<p>Before we talk about code, we think it's wise to learn about what features will be provided to our users:</p>
<ol>
<li><p>User can submit the code with type of runnable script or config file, more types coming soon. Additionally, user can add metadata like description and user steps (like custom input or change path).</p>
</li>
<li><p>After submission, modus call an async request. The request will detach user request, so no additional time to wait the request and we can run AI analysis in the background.</p>
</li>
<li><p>User can import script from URL, modus will get the scripts behind the scene and do the submission like usual.</p>
</li>
<li><p>User can browse uploaded scripts.</p>
</li>
<li><p>User can check scripts detail, including how to execute or get the config file, AI analysis result, AI and Community notes and of course the full scripts and wrapped scripts.</p>
</li>
<li><p>Additionally, you can ask AI to generate scripts for you.</p>
</li>
</ol>
<p>[Placeholder for code]</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Modus is really new but promising. In current state, we just wonder why not every Function as a Service done something like this. I’m amaze how fast I create the backend logic. Not mentioning this is my first time creating an API with LLM capabilities and compiling to WASM, even though all of it is abstracted by modus. Most of the time working in this project is mostly in the frontend side and read the internal of modus.</p>
<p>Here is summary of some feedback I mention above that I hope Hypermode can add in the future:</p>
<ul>
<li><p>RESTful API / API Gateway capabilities or built-in integration</p>
</li>
<li><p>Async / Event Based Request</p>
</li>
<li><p>Mixing public and private endpoint or schema</p>
</li>
<li><p>Monorepo deployment</p>
</li>
</ul>
<p>Nevertheless, it was really enjoyable journey. Thank You.</p>
<p>Nb: Even though in this article we use “we” as a author, in reality only me without a team doing all the things :D</p>
]]></content:encoded></item><item><title><![CDATA[Memperkenalkan BackMeUp - Cara Mudah Backup Foto dan Dokumen dari Whatsapp]]></title><description><![CDATA[Article in English will be release soon, but the current app is for Indonesian language only. If you are interested to use it, please add a comment below and I will add you to waiting list.

Bayangkan momen penting bersama keluarga...
Sebentar lagi m...]]></description><link>https://blog.tegar.my.id/memperkenalkan-backmeup-cara-mudah-backup-foto-dan-dokumen-dari-whatsapp</link><guid isPermaLink="true">https://blog.tegar.my.id/memperkenalkan-backmeup-cara-mudah-backup-foto-dan-dokumen-dari-whatsapp</guid><category><![CDATA[buildinpublic]]></category><dc:creator><![CDATA[Tegar Imansyah]]></dc:creator><pubDate>Tue, 26 Mar 2024 23:41:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1711497947920/d3c942b4-5ac5-48b9-85f3-3674a4f077d7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Article in English will be release soon, but the current app is for Indonesian language only. If you are interested to use it, please add a comment below and I will add you to waiting list.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1711495802475/86910b91-c7c7-403a-a755-75a9c42be080.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-bayangkan-momen-penting-bersama-keluarga"><strong>Bayangkan momen penting bersama keluarga...</strong></h2>
<p>Sebentar lagi mau Idul Fitri dan Anda bertemu dengan keluarga tercinta setelah sekian lama tidak bertemu. Anda tidak lupa untuk mengabadikan momen spesial tersebut dengan berfoto bersama dan dikirim lah oleh sepupu via Whatsapp Group. Sebulan kemudian Anda ingin melihat foto tersebut tapi ternyata sudah tidak ada dan lupa di download. Mau minta kirim lagi tapi sungkan atau malah fotonya juga sudah terhapus.</p>
<h2 id="heading-bayangkan-invoice-belum-terbayar-tapi-sudah-hilang"><strong>Bayangkan invoice belum terbayar tapi sudah hilang...</strong></h2>
<p>Anda ingin mengirimkan invoice atau dokumen penting lainnya ke client atau bos. Setelah berkali kali revisi, akhirnya dokumen final terkirim juga. Sekian purnama berikutnya, ternyata beliau minta dikirim ulang <strong>secepatnya</strong> karena akan ada audit dan kalian sedang liburan. Dokumen itu juga sudah tidak ada di hp maupun laptop, download juga sudah tidak bisa. Satu satunya cara adalah buat lagi, tapi lupa versi mana yang benar dan kebetulan sedang mudik ke kampung.</p>
<h2 id="heading-backmeup-membantu-anda-menyimpan-gambar-video-dokumen-dan-chat-dengan-mudah"><strong>BackMeUp membantu anda menyimpan Gambar, Video, Dokumen dan Chat dengan mudah</strong></h2>
<p><img src="https://storage.tally.so/a875a752-a63d-4668-813a-ae62c86ccb52/backmeup.jpg" alt="https://storage.tally.so/a875a752-a63d-4668-813a-ae62c86ccb52/backmeup.jpg" /></p>
<p>Forward Gambar, Video, Dokumen atau Chat ke Official Whatsapp Kami dan file akan tersimpan pada <strong>Google Drive Anda</strong>. Berbagai fitur tambahan lain akan diimplementasikan sesuai dengan kebutuhan Anda.</p>
<p><strong>Privacy Dijamin</strong>. Dengan sistem canggih kami, file yang Anda kirim tidak dapat disimpan atau dilihat selain oleh Anda.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/Tegar_Imansyah/status/1772398937976041584">https://twitter.com/Tegar_Imansyah/status/1772398937976041584</a></div>
<p> </p>
<p>Ayo bergabung menjadi Early Adopter dan dapatkan diskon hingga 67% dengan mendftar di link ini</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://backmeup.tegar.my.id">https://backmeup.tegar.my.id</a></div>
]]></content:encoded></item><item><title><![CDATA[Job Hunter Companion: AI that Review Your CV and Check Compability with Job Description]]></title><description><![CDATA[A New Year Brings New Job Opportunities
Or so they say. It has become an annual event where everyone is on the lookout for a fresh job. On my LinkedIn timeline, I've noticed numerous individuals beginning to use the "open to work" badge. Surprisingly...]]></description><link>https://blog.tegar.my.id/job-hunter-companion-ai-that-review-your-cv-and-check-compability-with-job-description</link><guid isPermaLink="true">https://blog.tegar.my.id/job-hunter-companion-ai-that-review-your-cv-and-check-compability-with-job-description</guid><category><![CDATA[mindsdb]]></category><category><![CDATA[MindsDBHackathon]]></category><dc:creator><![CDATA[Tegar Imansyah]]></dc:creator><pubDate>Mon, 15 Jan 2024 08:00:37 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-a-new-year-brings-new-job-opportunities">A New Year Brings New Job Opportunities</h2>
<p>Or so they say. It has become an annual event where everyone is on the lookout for a fresh job. On my LinkedIn timeline, I've noticed numerous individuals beginning to use the "open to work" badge. Surprisingly, within just the second week of the new year, there has been a wave of layoffs spanning from small companies to large global tech firms. This realization has led me to believe that perhaps it's time for people to revisit their resumes and LinkedIn profiles to align them with their current circumstances. So, how about letting AI assist you with that?</p>
<p><a target="_blank" href="https://www.linkedin.com/feed/update/urn:li:activity:7149397245115719680/"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705298956232/b66914bd-b03d-4698-8379-4f9912c594af.png" alt class="image--center mx-auto" /></a></p>
<p>I simply wrote an MVP using a Chrome Extension for user interaction and MindsDB for the database and AI. I then shared my work on LinkedIn to validate the idea, and to my surprise, it garnered the highest impression I have ever received. Alright, let's proceed with building it.</p>
<h2 id="heading-the-user-journey">The User Journey</h2>
<p>Here is how you, as a user, will use Job Hunter Companion (JHC) Chrome Extension.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705300805113/a7a0fb93-faf6-4cdd-bef3-aa57c38dbb40.png" alt class="image--center mx-auto" /></p>
<p>It's not like other ideas that create their own job marketplace, here you can improve your profile and use that to job description in all website. Just a single click and it will give you constructive feedback and can generate custom cover letter.</p>
<h2 id="heading-getting-started-with-chrome-extension">Getting Started with Chrome Extension</h2>
<p>The scope of this part is only running the extension in local. I use React, Vite and tailwind for this frontend stack. The main different when we build a normal web app and chrome extension is we can use <a target="_blank" href="https://developer.chrome.com/docs/extensions/reference/api">Chrome API</a> only when we load it as extension. So the normal development server will not help us. Here is some changes I mad:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"vite"</span>,
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"vite build --watch"</span>,
  }
}
</code></pre>
<p>Look that I add <code>--watch</code> in vite build. So instead of running <code>bun run dev</code>, I run <code>bun run build</code> and it still looking for changes and build the app immediately.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// vite.config.js</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig({
  <span class="hljs-attr">build</span>: {
    <span class="hljs-attr">outDir</span>: <span class="hljs-string">"/home/tegar/data/hackathon/mindsdb/dist"</span>,
    <span class="hljs-attr">emptyOutDir</span>: <span class="hljs-literal">true</span>
  },
  ...
});
</code></pre>
<p>Since I use WSL, I need to put my project in Linux filesystem so the file watcher will work, but I also need to load the output to the Windows filesystem. The config above will write the output to windows filesystem (<code>data</code> is a symlink to drive <code>D:</code>).</p>
<h2 id="heading-proxy-backend-server">Proxy Backend Server</h2>
<p>Since the MindsDB opensource have no authentication system, so it's not a best practice to call a request from client side. So I build a simple proxy backend using express that will handle complex query to MindsDB and save the CV as pdf in local disk. We will talk about why we need to save the pdf later.</p>
<p>There is nothing special in the code, just run <code>bun run index.js</code> and it will serve the backend.</p>
<h2 id="heading-the-main-part-mindsdb">The Main Part: MindsDB</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1705305106135/b781439e-dc85-40b2-ab71-51c0c90ada3d.png" alt class="image--center mx-auto" /></p>
<p>First we need to prepare MindsDB dependency: web crawler and OpenAI</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">DATABASE</span> web 
<span class="hljs-keyword">WITH</span> <span class="hljs-keyword">ENGINE</span> = <span class="hljs-string">'web'</span>;

<span class="hljs-keyword">CREATE</span> ML_ENGINE openai_engine
<span class="hljs-keyword">FROM</span> openai
<span class="hljs-keyword">USING</span>
    api_key = <span class="hljs-string">'&lt;your-openai-key&gt;'</span>;
</code></pre>
<p>After that we test how we get the resume from pdf and get the text from job posting</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Get Text from PDF served by local server</span>
<span class="hljs-keyword">SELECT</span> * 
<span class="hljs-keyword">FROM</span> web.crawler 
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">url</span> = <span class="hljs-string">'http://docker.host.internal:3000/resume'</span> 
<span class="hljs-keyword">LIMIT</span> <span class="hljs-number">1</span>;

<span class="hljs-comment">-- Get Text from website</span>
<span class="hljs-keyword">SELECT</span> * 
<span class="hljs-keyword">FROM</span> web.crawler 
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">url</span> = <span class="hljs-string">'https://&lt;job-post-url&gt;'</span> 
<span class="hljs-keyword">LIMIT</span> <span class="hljs-number">1</span>;
</code></pre>
<p>As you can see, I get the PDF from the express server. It will return the PDF directly. MindsDB can understand whether we put a web or pdf.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This is a first step to help many people to improve their profile and understand better about how match they profile and their desire job description. In the future if this MVP got more traction (and win the competition 😁) I will publish the extension to the Chrome Web Store and create a SaaS for that. So everyone, especially non-tech fellow, doesn't need to install and run it by himself.</p>
<p>This post is part of hackathon hosted by <a target="_blank" href="https://hashnode.com/">Hashnode</a> and <a target="_blank" href="https://mindsdb.com/">MindsDB</a>. Thank you for creating this awesome opportunity.</p>
]]></content:encoded></item><item><title><![CDATA[Building an CLI for Streamlining Server Access Management with Pangea Vault and AuthN]]></title><description><![CDATA[Why I Made It
As a developer constantly working with various servers, I often found myself grappling with the challenge of managing SSH configurations. The combination of SSH keys, usernames, and server IPs became a mental jigsaw puzzle, leading to f...]]></description><link>https://blog.tegar.my.id/building-an-cli-for-streamlining-server-access-management-with-pangea-vault-and-authn</link><guid isPermaLink="true">https://blog.tegar.my.id/building-an-cli-for-streamlining-server-access-management-with-pangea-vault-and-authn</guid><category><![CDATA[pangea]]></category><category><![CDATA[PangeaSecurathon]]></category><dc:creator><![CDATA[Tegar Imansyah]]></dc:creator><pubDate>Thu, 16 Nov 2023 07:04:21 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-why-i-made-it">Why I Made It</h2>
<p>As a developer constantly working with various servers, I often found myself grappling with the challenge of managing SSH configurations. The combination of SSH keys, usernames, and server IPs became a mental jigsaw puzzle, leading to frequent instances of forgetting or misplacing crucial information. This inconvenience not only slowed down my workflow but also posed a security risk as I juggled between different configurations.</p>
<p>To address this issue and enhance my efficiency, I decided to create an SSH Manager CLI. The primary goal was to streamline the process of managing SSH configurations, making it easier to recall and access the necessary information securely.</p>
<h2 id="heading-ssm-in-action">SSM In Action</h2>
<p>Since this is a CLI tools, so there is no web app demo. I also prepared a web app, but it's only backend where CLI interacts with it for store and get from Pangea Vault. If you want to try it, please try in this github repo: <a target="_blank" href="https://github.com/tegarimansyah/secure-server-manager">https://github.com/tegarimansyah/secure-server-manager</a></p>
<p>or if you prefer a video demo (but please run 1.5x or 2x speed).</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/Ts39Juo6VDU">https://youtu.be/Ts39Juo6VDU</a></div>
<p> </p>
<h2 id="heading-the-build-journey">The Build Journey</h2>
<h3 id="heading-tech-stack">Tech Stack</h3>
<ul>
<li><p>Main CLI app</p>
<ul>
<li><p>Python</p>
</li>
<li><p>Typer</p>
</li>
<li><p>FastApi (for accepting callback from Pangea AuthN)</p>
</li>
<li><p>httpx (for calling Pangea and Backend API)</p>
</li>
<li><p>rich and questionary (for interactivity)</p>
</li>
</ul>
</li>
<li><p>Backend API</p>
<ul>
<li>Javascript in Cloudflare Worker</li>
</ul>
</li>
</ul>
<p>We need a backend API to use Pangea Vault. It's because the CLI is client-side, so there is no way to use server-side token to store and get vault data. We also can't use user authentication token since there is no relation between the user and the vault, maybe later Pangea should integrate both features more. But don't worry, there is a workaround I found below.</p>
<h3 id="heading-sequence-diagram">Sequence Diagram</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700233397518/b516c300-a9c4-41d4-99ac-48280432995b.png" alt class="image--center mx-auto" /></p>
<p>First of all, we need to login. Running <code>ssm login</code> will open your default browser and go to Pangea hosted login. Behind the scene, it will also create a server instance waiting for a callback from Pangea. After we login (or sign up), it will redirect to local server and send a code. Local server will receive the code and call Pangea AuthN API to receive user profile (<code>/client/userinfo</code>). Honestly, this process took me a while and I found this method by looking at <a target="_blank" href="https://github.com/pangeacyber/pangea-javascript/blob/main/packages/vanilla-js/src/AuthNClient/index.ts#L42">js sdk</a> .</p>
<p>After that, CLI will call backend API to get <code>server.json</code> where configuration is saved and keys from Pange vault. To make sure we got correct <code>server.json</code> and <code>keys</code>, I utilized the user id as a <a target="_blank" href="https://pangea.cloud/docs/vault/using-vault-ui/vault-interface#manage-folders">folder name</a>. In the API, we can get secrets by using <a target="_blank" href="https://pangea.cloud/docs/api/vault#list">list</a> endpoint with filtering <code>folder</code> as <code>user_id</code> and <code>name</code> as <code>server.json</code> and <code>include_secret</code>. For keys, I save them in a folder <code>&lt;user_id&gt;/keys</code> and without name (so we will get all files). Here is the example API using curl:</p>
<pre><code class="lang-bash">curl -sSLX POST <span class="hljs-string">'https://vault.aws.us.pangea.cloud/v1/list'</span> \
-H <span class="hljs-string">'Authorization: Bearer &lt;server_side_token&gt;'</span> \
-H <span class="hljs-string">'Content-Type: application/json'</span> \
-d <span class="hljs-string">'{"filter":{"folder":"&lt;user_id&gt;","name":"server.json"},"include_secrets":true}'</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700233405823/da2e70b8-4edf-46bd-bb29-982c4bbb3f5a.png" alt class="image--center mx-auto" /></p>
<p>After we login, we get <code>server.json</code> and keys from previously saved session. But if we just getting started, we can use <code>ssm add</code> to add new server configuration. It will sync to pangea cloud so the next time we login, we get our latest configuration.</p>
<p>After that for main command <code>ssm ssh</code>, it will only check the <code>server.json</code> and then run ssh command to connect to server. So there is connection to pange or backend api.</p>
<h3 id="heading-cli-setup">CLI Setup</h3>
<p>I call my tool Secure SSH Manager, or <code>ssm</code> in short. After cloning the repo, install the dependency using <code>poetry install</code>. I just remove the extension, <code>chmod +x ssm</code> and added shebang <code>#!/usr/bin/env python3</code>. After that I can invoke the command inside poetry virtual environment.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700096516271/32650b4c-8104-4554-bcc0-8a83e3585fd6.png" alt class="image--center mx-auto" /></p>
<p>This beautiful help is auto-generated by typer. It can also open your default browser and start a fastapi server locally.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700117165766/ae40a849-2dd5-4cdd-a4e4-df572cbe7566.png" alt class="image--center mx-auto" /></p>
<p>After we log in, the same command will show us the profile that we logged into. The cool thing when we build with pangea is how interactive the API docs are. Look at this page, you can use your existing token, add only required params or also add optional params.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700237327210/9f0251ab-1a61-4024-89f1-b41535c4c7be.png" alt class="image--center mx-auto" /></p>
<p>Okay back to the cli. Behind the scene after we login, we also get the stored <code>server.json</code> and keys in vault copied to local.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700243667281/be1705bf-abba-44d5-9489-95e5013a8b71.png" alt class="image--center mx-auto" /></p>
<p>But what if we need to add a new server for initial data? We use <a target="_blank" href="https://questionary.readthedocs.io/en/stable/">questionary</a> to interactive create user prompts.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700117740847/8b422b20-c557-4019-9ed8-92ba29eafca7.png?auto=compress,format&amp;format=webp" alt /></p>
<p>Lastly, we will ssh to the server</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700118028995/74d1bfc8-0fe7-42d7-8a3b-de234cd9cb7d.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-backend-setup">Backend Setup</h3>
<p>Since I only need this backend as a proxy to call Pangea to store and get the secret without giving token to client side, I need to make it as simple as possible. First I think I need a framework like Django or Svelte (well, with beautiful frontend or sophisticated backend with ease). But for first MVP, I don't need it, even I don't need a database.</p>
<p>So my choice is to use cloudflare worker. It's really easy that I only use built in editor to make it. First of all I add environment variables.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700105465694/08f196ba-d9ba-4a0e-9efa-23191741f028.png" alt class="image--center mx-auto" /></p>
<p>And here is the list of endpoints. All endpoints need a JWT token and check via Pangea AuthN, so we can get the user_id (or reject the request if not authenticated)</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Endpoint</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td>POST /server-config</td><td>Receive <code>application/json</code> of server configuration content, and create or update <code>server.json</code> in <code>user_id</code> folder.</td></tr>
<tr>
<td>GET /server-config</td><td>Return content of <code>server.json</code></td></tr>
<tr>
<td>POST /private-keys?filename=&lt;filename&gt;</td><td>Receive <code>plain/text</code> of private key content, and create or update <code>&lt;filename&gt;</code> in <code>&lt;user_id&gt;/keys</code> folder</td></tr>
<tr>
<td>GET /private-keys</td><td>Return all files in <code>&lt;user_id&gt;/keys</code> folder</td></tr>
</tbody>
</table>
</div><h2 id="heading-what-i-achieved"><strong>What I Achieved</strong></h2>
<p>In conclusion, it's a lot easier to build the SSH Manager CLI with Pangea's AuthN and Vault. By centralizing and securing SSH configurations, I've eliminated the risk of forgetting or misplacing crucial information. The step-by-step synchronization and selection process ensures a seamless and secure user experience.</p>
<p>This project stands as a testament to the power of innovation when coupled with cutting-edge security solutions. With Pangea, I've successfully turned the complexity of server access into a simple, efficient, and secure process. Now, developers can focus on their app logic without compromising on security, thanks to the simplified set of APIs provided by Pangea.</p>
<p>Next time, I will add a encryption key, so everything that uploaded to server will be encrypted first in local. So user can store their private key in peace.</p>
<p>Thank you <a target="_blank" href="https://pangea.cloud">pangea</a> and <a target="_blank" href="https://hashnode.com/">hashnode</a> for bringing this hackathon.</p>
]]></content:encoded></item><item><title><![CDATA[Build Powerful and Secure Multiple Outerbase Commands With PNPM Workspace and Vite]]></title><description><![CDATA[Note: Outerbase is still in beta by the time this article was written. My approach and solution below may need adjustment when future features and support, such as getting HTTP header requests and exit chaining of nodes, are available.
Note 2: This a...]]></description><link>https://blog.tegar.my.id/build-powerful-and-secure-multiple-outerbase-commands-with-pnpm-workspace-and-vite</link><guid isPermaLink="true">https://blog.tegar.my.id/build-powerful-and-secure-multiple-outerbase-commands-with-pnpm-workspace-and-vite</guid><category><![CDATA[Outerbase]]></category><category><![CDATA[outerbasehackathon]]></category><dc:creator><![CDATA[Tegar Imansyah]]></dc:creator><pubDate>Sun, 24 Sep 2023 05:17:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1695560754377/0daac054-178a-47f4-b795-4a5f9c88be99.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Note:</em> Outerbase is still in beta by the time this article was written. My approach and solution below may need adjustment when future features and support, such as <a target="_blank" href="https://discord.com/channels/1123612147704934400/1153861145636122634">getting HTTP header requests</a> and <a target="_blank" href="https://discord.com/channels/1123612147704934400/1153753811001810954">exit chaining of nodes</a>, are available.</p>
<p><em>Note 2:</em> This article is still a work in progress that expected to finish on September 26th. You may see something like <code>&lt;Placeholder for xxx&gt;</code> or <code>TBA</code>(To Be Added). Follow me or check periodically to know more updates.</p>
<hr />
<p><a target="_blank" href="https://outerbase.com/">Outerbase</a> is like a dbeaver or phpmyadmin, where you can connect your database, then you can browse tables and query, insert, update and delete your data easily. But not like those two, Outerbase doesn't require you to install anything, you can customize the look and feel (not only table like spreadsheet) and can manipulate the data via Javascript, SQL and Python through RESTful API. The ability to create an API without worrying about backend deployment is called <a target="_blank" href="https://docs.outerbase.com/docs/commands/intro-to-commands"><strong>command</strong></a> in Outerbase. In this article, we will try to use the command, deep dive into some usage and current limitations and see what am I prepared for you.</p>
<p><em>TLDR: I prepared how to build a command with splitting files and 3rd party library (+ GitHub repo template -- check below), avoid writing a hard-coded secret to call 3rd party services, and protect an endpoint so not everyone can access the data. Everything while we create some sample integration.</em> <strong>See the demo video below.</strong></p>
<p><strong>&lt;placeholder for demo video&gt;</strong></p>
<p>Github for repo template: <a target="_blank" href="https://github.com/tegarimansyah/outerbase-commands-workspace">outerbase-commands-workspace</a></p>
<p>Github for complete command with integrations: <strong>&lt;placeholder for github url&gt;</strong></p>
<p>In this article, we will mostly talk about the backend side, thus we will talk about Outerbase command, database configuration and integration with several 3rd parties that help us finish our job. List of integrations:</p>
<ul>
<li><p>PNPM and Vite for <a target="_blank" href="https://blog.tegar.my.id/build-powerful-and-secure-multiple-outerbase-commands-with-pnpm-workspace-and-vite#heading-using-pnpm-workspace-and-vite">building complex node</a></p>
</li>
<li><p>Supabase Vault for <a target="_blank" href="https://blog.tegar.my.id/build-powerful-and-secure-multiple-outerbase-commands-with-pnpm-workspace-and-vite#heading-encrypt-secret-in-database-with-supabase-vault-use-case-to-integrate-with-aws">storing secrets</a> (+ AWS service example)</p>
</li>
<li><p>Token-based and JWT for <a target="_blank" href="https://blog.tegar.my.id/build-powerful-and-secure-multiple-outerbase-commands-with-pnpm-workspace-and-vite#heading-protect-endpoint-with-token-based-authentication-and-jwt-use-case-with-clerk">the protected endpoint</a> (+ Clerk integration)</p>
</li>
<li><p>Qstash from Upstash for <a target="_blank" href="https://blog.tegar.my.id/build-powerful-and-secure-multiple-outerbase-commands-with-pnpm-workspace-and-vite#heading-scheduling-job-to-remind-sales-person-with-upstash">the delayed message</a></p>
</li>
<li><p>Ntfy for <a target="_blank" href="https://blog.tegar.my.id/build-powerful-and-secure-multiple-outerbase-commands-with-pnpm-workspace-and-vite#heading-add-push-notification-if-a-deal-is-closed-with-ntfy">Push notification</a></p>
</li>
<li><p>Mailgun for <a target="_blank" href="https://blog.tegar.my.id/build-powerful-and-secure-multiple-outerbase-commands-with-pnpm-workspace-and-vite#heading-automatic-monthly-report-send-to-email-with-mailgun">Monthly report email</a></p>
</li>
<li><p>Axiom for <a target="_blank" href="https://blog.tegar.my.id/build-powerful-and-secure-multiple-outerbase-commands-with-pnpm-workspace-and-vite#heading-app-and-audit-log-for-debugging-with-axiom">app log and audit log</a> (help me as developer)</p>
</li>
</ul>
<h2 id="heading-preparation">Preparation</h2>
<h3 id="heading-idea-and-prepare-some-data">Idea and Prepare Some Data</h3>
<p>Some weeks ago, I had a chat with my father. He wants to track his salespeople visits to several selected stores in the same region. For that, he needs to have salesperson data, store data and a list of stores that the salesperson needs to visit every day. When salesperson arrive in the store and do their things, they need to check in by filling form and capturing a photo. My father as a manager will see a dashboard that shows an overview of all salespeople's progress in real-time.</p>
<p>For that purpose, I will use a PostgreSQL database provided by <a target="_blank" href="https://supabase.com/">Supabase</a>. Connecting Outerbase and Supabase is really easy, but we will look further to make use of both features to the fullest. For this demo, I will use some dummy data that represents actual data. You can see the data in Google Sheet: <a target="_blank" href="https://docs.google.com/spreadsheets/d/1gnyEaF6oJ8XNrOWkWzd9ZdwXkA6ivgJMfAF-QTSbijM/edit?usp=sharing">https://docs.google.com/spreadsheets/d/1gnyEaF6oJ8XNrOWkWzd9ZdwXkA6ivgJMfAF-QTSbijM/edit?usp=sharing</a></p>
<p><a target="_blank" href="https://docs.google.com/spreadsheets/d/1gnyEaF6oJ8XNrOWkWzd9ZdwXkA6ivgJMfAF-QTSbijM/edit?usp=sharing"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695463440578/1adf896c-31fe-4e03-aae7-5f93bc5f9e8a.png" alt class="image--center mx-auto" /></a></p>
<p>-- Image: Example Data for Our Internal Tool</p>
<h3 id="heading-prepare-database-and-import-data">Prepare Database and Import Data</h3>
<p>You can get a PostgreSQL database for free in Supabase. For connecting supabase with Outerbase, you can follow <a target="_blank" href="https://docs.outerbase.com/docs/providers/supabase">official docs of Outerbase</a> or <a target="_blank" href="https://rockyessel.hashnode.dev/connecting-outerbase-to-supabase-postgres">Follow the steps</a> for more visuals provided by my fellow tech writer.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695471626485/e29fce6d-f73d-4f61-86c2-a0249096bcc6.png" alt class="image--center mx-auto" /></p>
<p>-- Image: Outerbase Screen After You Connect to a Database</p>
<p>After you get a database instance, now is your time to create some tables and provide some data. Download the Google Sheet above as two CSV files, one for salesperson and one for store. With Supabase, you can import data from CSV. Open your <strong>Project -&gt; Database -&gt; + New Table -&gt; (in Columns section) import data via spreadsheet</strong>. Just drop the file here, give it a table name, set the type of data and you will see the column and data will be automatically created.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695471997013/d3d5bfaf-6ddc-4143-aa41-a3563684ef81.png" alt class="image--center mx-auto" /></p>
<p>-- Image: Create a Table By Importing a CSV in Supabase</p>
<p>After the import process is finished, we can go back to Outerbase and create some queries. Go to your database and click <strong>New</strong> -&gt; <strong>Query</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695473777627/146238cd-d19d-40f5-9fc6-1a39436d00cb.png" alt class="image--center mx-auto" /></p>
<p>-- Image: Outerbase Show Our Newly Created Table. Now We Will Create a Query Manually or Via EZQL</p>
<p>If you are familiar with Metabase or Redash, you normally write your own SQL to get the data. But Outerbase gives us a fresh way to get the data, it's called <strong>EZQL</strong>, you just write what question you want to ask about the data and it will generate the SQL and give you the data. Just like ChatGPT, but it knows the context of data. Let's give it a try by asking about how many salespersons and stores are in my database and the ratio of store to salesperson.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695475199484/33b09d77-c4b6-4cb9-8bfc-4f556ae45855.png" alt class="image--center mx-auto" /></p>
<p>-- Image: It took me 3 prompts (1 initial, 1 revise and 1 to optimize) until I got the expected result.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- prompt: I want to know how many I have salesperson, store and ratio of store to salesperson</span>
<span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">COUNT</span>(*) <span class="hljs-keyword">as</span> salesperson_count <span class="hljs-keyword">FROM</span> public.salesperson

<span class="hljs-comment">-- prompt: well, that only count the salesperson, I also want to see store and the ratio</span>
<span class="hljs-keyword">SELECT</span>
  (<span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">COUNT</span>(*) <span class="hljs-keyword">FROM</span> public.salesperson) <span class="hljs-keyword">AS</span> salesperson_count,
  (<span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">COUNT</span>(*) <span class="hljs-keyword">FROM</span> public.store) <span class="hljs-keyword">AS</span> store_count,
  (
    <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">COUNT</span>(*)::<span class="hljs-built_in">float</span> / (<span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">COUNT</span>(*) <span class="hljs-keyword">FROM</span> public.salesperson) <span class="hljs-keyword">FROM</span> public.store
  ) <span class="hljs-keyword">AS</span> store_salesperson_ratio

<span class="hljs-comment">-- prompt: That cool! But I see we query each table twice. Can't we do that only once?</span>
<span class="hljs-keyword">SELECT</span> salesperson_count, store_count, store_count::<span class="hljs-built_in">float</span> / salesperson_count <span class="hljs-keyword">AS</span> store_salesperson_ratio
<span class="hljs-keyword">FROM</span>
  (
    <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">COUNT</span>(*) <span class="hljs-keyword">AS</span> salesperson_count
    <span class="hljs-keyword">FROM</span> public.salesperson
  ) sp,
  (
    <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">COUNT</span>(*) <span class="hljs-keyword">AS</span> store_count
    <span class="hljs-keyword">FROM</span> public.store
  ) st
</code></pre>
<p>Great! So I know there is 30 salesperson and each one of them needs to handle 16-17 stores, not ideal BTW. Side note: I also ask ChatGPT to revise my first SQL, but there is no luck since it doesn't understand the table schema.</p>
<p>Okay, let's go with our next step. We need to create an agenda table that stores a list of salesperson visits. Let's give EZQL a try.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- prompt: create a new table called "agenda" that have relation to salesperson and store, </span>
<span class="hljs-comment">--         and also add a columns: visit_date, status. And don't forget uuid as primary key id.</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> agenda (
  <span class="hljs-keyword">id</span> <span class="hljs-keyword">uuid</span> PRIMARY <span class="hljs-keyword">KEY</span>,
  salesperson_id <span class="hljs-keyword">uuid</span> <span class="hljs-keyword">REFERENCES</span> public.salesperson(<span class="hljs-keyword">Id</span>),
  store_id <span class="hljs-keyword">uuid</span> <span class="hljs-keyword">REFERENCES</span> public.store(<span class="hljs-keyword">Id</span>),
  visit_date <span class="hljs-built_in">date</span>,
  <span class="hljs-keyword">status</span> <span class="hljs-built_in">varchar</span>(<span class="hljs-number">255</span>)
);
</code></pre>
<p>Looks good, but unfortunately, that returns an error. By the time this article was written, Outerbase hang and didn't throw an error. But, I believe they will fix it soon before graduate from beta version. Fortunately, I can get the error log from Supabase.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695478963547/ae0e2d84-7004-425f-9df9-52a892d61332.png" alt class="image--center mx-auto" /></p>
<p>It took me some time to resolve the error, by using EZQL, ChatGPT, StackOverflow and in the end, I resolved it by making my hand dirty with direct trial error with SQL Query. The solution is to use quote to <code>Id</code> in reference definition. So, instead of <code>public.salesperson(Id)</code> I need to write <code>public.salesperson("Id")</code>. Maybe that's because I use capital in the column name (since the CSV is like that and I was too lazy to change it), please leave a comment if you know about this.</p>
<h3 id="heading-removing-unused-table-from-outerbase">Removing Unused Table from Outerbase</h3>
<p>Supabase already created some tables for it's internal and we don't need it for our app. And because we use a root account (postgres) for Outerbase, it will get all schema and tables. For my non-technical father, it will be too much!</p>
<p>You may need to read about <a target="_blank" href="https://docs.outerbase.com/docs/connections/understanding-access-roles">Access Roles</a> to understand how Outerbase will behave with your data. TLDR, we will create new Postgres roles (accounts) that have access only to our table.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">role</span> <span class="hljs-string">"outerbase"</span> login <span class="hljs-keyword">password</span> <span class="hljs-string">'your-secret-password'</span>;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">insert</span>, <span class="hljs-keyword">select</span>, <span class="hljs-keyword">update</span> <span class="hljs-keyword">on</span> <span class="hljs-keyword">all</span> <span class="hljs-keyword">tables</span> <span class="hljs-keyword">in</span> <span class="hljs-keyword">schema</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">to</span> outerbase;
</code></pre>
<p>After that, we can change the connection setting in Outerbase (In your project, go to <strong>Settings -&gt; Database</strong>) and see if we only see tables for our app.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695486669907/83c2e4b0-ad6b-4df3-801f-0596073c4c67.png" alt class="image--center mx-auto" /></p>
<p>-- Image: Before you will see all schema, but after login with new roles and granting only specific permission, the table will be neater.</p>
<blockquote>
<p>Notes: You may want to <a target="_blank" href="https://supabase.com/docs/guides/auth/row-level-security">disable RLS (Row Level Security)</a> or create a new policy for Outerbase to access the data. If not, you will not be able to get the data.</p>
</blockquote>
<h2 id="heading-creating-an-outerbase-command-to-manipulate-data">Creating an Outerbase Command to Manipulate Data</h2>
<p>Okay, we have data and access to it via query. Now is the most interesting part: creating a logic and manipulating our data with Outerbase Command.</p>
<h3 id="heading-hello-world-for-command">Hello World for Command</h3>
<p>First of all, go create a new command.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695487099985/d956274b-5646-40bf-a631-75e340bdb8fd.png" alt class="image--center mx-auto" /></p>
<p>A command is a single endpoint path (e.g. <code>/hello</code>) that can be called using GET, POST, PUT and DELETE verbs. After we hit the endpoint, a node will be processing the request. If we have more than one node, it will execute the next node -- let's call it <code>node-1</code>, <code>node-2</code>, <code>node-3</code>, and so on -- sequentially until the last node returns a response to the user. <code>node-2</code> can get a return value from <code>node-1</code> and <code>node-3</code> can get return values from <code>node-1</code> and <code>node-2</code>. All nodes can get data from <code>request.body</code> and <code>request.query</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695489451049/0eac4ed0-ff2c-48ab-8c73-e9b52087e195.png" alt class="image--center mx-auto" /></p>
<p>To make you understand the process, I created a simple hello world for testing the result.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695530215405/cc7fa458-ac3f-4091-8eb5-b2ba334e5949.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Node name: Node 1</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userCode</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">from</span>: <span class="hljs-string">"node-1"</span>,
        <span class="hljs-attr">body</span>: {{request.body}},
        <span class="hljs-attr">query</span>: {{request.query.token}}
    }
}

<span class="hljs-comment">// Node name: My Custom Name</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userCode</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">from</span>: <span class="hljs-string">"node-2"</span>,
        <span class="hljs-attr">body</span>: {{request.body}},
        <span class="hljs-attr">query</span>: {{request.query.token}},
        <span class="hljs-string">"node-1"</span>: {
            <span class="hljs-attr">full</span>: {{node<span class="hljs-number">-1</span>}},
            <span class="hljs-attr">from</span>: {{node<span class="hljs-number">-1.</span><span class="hljs-keyword">from</span>}},
            <span class="hljs-attr">body</span>: {{node<span class="hljs-number">-1.</span>body}},
            <span class="hljs-attr">query</span>: {{node<span class="hljs-number">-1.</span>query}}
        }
    }
}

<span class="hljs-comment">// Node name: Node 3</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userCode</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">from</span>: <span class="hljs-string">"node-3"</span>,
        <span class="hljs-attr">body</span>: {{request.body}},
        <span class="hljs-attr">query</span>: {{request.query.token}},
        <span class="hljs-string">"node-1"</span>: {
            <span class="hljs-attr">full</span>: {{node<span class="hljs-number">-1</span>}},
            <span class="hljs-attr">from</span>: {{node<span class="hljs-number">-1.</span><span class="hljs-keyword">from</span>}},
            <span class="hljs-attr">body</span>: {{node<span class="hljs-number">-1.</span>body}},
            <span class="hljs-attr">query</span>: {{node<span class="hljs-number">-1.</span>query}}
        },
        <span class="hljs-string">"node-2"</span>: {
            <span class="hljs-attr">full</span>: {{my-custom-name}},
            <span class="hljs-attr">from</span>: {{my-custom-name.from}},
            <span class="hljs-attr">body</span>: {{my-custom-name.body}},
            <span class="hljs-attr">query</span>: {{my-custom-name.query}}
        }
    }
}
</code></pre>
<p>Keep in mind, that's not a valid javascript so you will find your linter will complain. Don't worry, we let Outerbase replace the variable before executing it as a valid javascript. See my workaround using PNPM workspace and vite in the below section for the solution.</p>
<p>And here is the result</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"from"</span>: <span class="hljs-string">"node-3"</span>,
  <span class="hljs-attr">"body"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"query"</span>: <span class="hljs-string">"test"</span>,
  <span class="hljs-attr">"node-1"</span>: {
    <span class="hljs-attr">"full"</span>: <span class="hljs-string">"{\"from\":\"node-1\",\"body\":\"\",\"query\":\"test\"}"</span>,
    <span class="hljs-attr">"from"</span>: <span class="hljs-string">"node-1"</span>,
    <span class="hljs-attr">"body"</span>: <span class="hljs-string">""</span>,
    <span class="hljs-attr">"query"</span>: <span class="hljs-string">"test"</span>
  },
  <span class="hljs-attr">"node-2"</span>: {
    <span class="hljs-attr">"full"</span>: <span class="hljs-string">"{\"from\":\"node-2\",\"body\":\"\",\"query\":\"test\",\"node-1\":{\"full\":\"{\\\"from\\\":\\\"node-1\\\",\\\"body\\\":\\\"\\\",\\\"query\\\":\\\"test\\\"}\",\"from\":\"node-1\",\"body\":\"\",\"query\":\"test\"}}"</span>,
    <span class="hljs-attr">"from"</span>: <span class="hljs-string">"node-2"</span>,
    <span class="hljs-attr">"body"</span>: <span class="hljs-string">""</span>,
    <span class="hljs-attr">"query"</span>: <span class="hljs-string">"test"</span>
  }
}
</code></pre>
<p>As you can see if you directly refer to <code>{{node-1}}</code>, the return will be a string of JSON. So you need to parse it first using <code>JSON.parse({{node-1}})</code>. And make sure there is no quote before and after <code>{{node-1}}</code> since it will break the template mechanism by Outerbase.</p>
<p>And if you see directly, if I rename a node, then I will need to rename how I call it. For example, I call the second node <code>{{my-custom-name}}</code>, not <code>{{node-2}}</code>, since the name of the second node is <code>My Custom Name</code>.</p>
<h3 id="heading-accessing-data-with-sql">Accessing Data with SQL</h3>
<p>We can use any SQL command and add input from request data or previous nodes. The use case is not only for reading data from db, but we can also insert, update or delete data.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695492952370/d87eb686-6193-4b0e-a731-33bbe74e0370.png" alt class="image--center mx-auto" /></p>
<p>Hit the endpoint <code>/hello/world?token=test&amp;limit=2</code> will bring the result of the SQL node like this</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"success"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"response"</span>: {
    <span class="hljs-attr">"items"</span>: [
      {
        <span class="hljs-attr">"Id"</span>: <span class="hljs-string">"7036b866-e2f8-4b34-a7d8-88e0b6e523db"</span>,
        <span class="hljs-attr">"Name"</span>: <span class="hljs-string">"Erin Vargas"</span>,
        <span class="hljs-attr">"Email"</span>: <span class="hljs-string">"oscott@example.org"</span>,
        <span class="hljs-attr">"Phone"</span>: <span class="hljs-string">"+6281267188370"</span>,
        <span class="hljs-attr">"Avatar"</span>: <span class="hljs-string">"https://i.pravatar.cc/100?u=oscott@example.org"</span>
      },
      {
        <span class="hljs-attr">"Id"</span>: <span class="hljs-string">"42a4331c-8e85-43a6-afbf-1791b963e909"</span>,
        <span class="hljs-attr">"Name"</span>: <span class="hljs-string">"Dr. Susan Combs DDS"</span>,
        <span class="hljs-attr">"Email"</span>: <span class="hljs-string">"victoria95@example.com"</span>,
        <span class="hljs-attr">"Phone"</span>: <span class="hljs-string">"+6281280966669"</span>,
        <span class="hljs-attr">"Avatar"</span>: <span class="hljs-string">"https://i.pravatar.cc/100?u=victoria95@example.com"</span>
      }
    ],
    <span class="hljs-attr">"schema"</span>: <span class="hljs-literal">false</span>
  }
}
</code></pre>
<p>We can use this result for the next node, in this case, I use Javasript node. Please keep in mind if you pass directly from <code>{{node-name}}</code> then you need to parse it first. For that, usually, I create a helper function like this:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">parseResult</span>(<span class="hljs-params">resultStr</span>) </span>{
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> result = <span class="hljs-built_in">JSON</span>.parse(resultStr)
        <span class="hljs-keyword">if</span> (!result.success) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>
        <span class="hljs-keyword">return</span> result.response.items
    } <span class="hljs-keyword">catch</span> {
        <span class="hljs-comment">// For now, if the SQL throw error, the return is not a valid JSON</span>
        <span class="hljs-comment">// so we will catch it here</span>
        <span class="hljs-comment">// bug fix in progress by Outerbase team</span>
        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>
    }
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">parseResultOne</span>(<span class="hljs-params">resultStr</span>) </span>{
    <span class="hljs-keyword">const</span> result = parseResult(resultStr)
    <span class="hljs-keyword">if</span> (result === <span class="hljs-literal">null</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>
    <span class="hljs-keyword">return</span> result.response.items[<span class="hljs-number">0</span>]
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userCode</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> result = parseResultOne({{node<span class="hljs-number">-1</span>}})
    <span class="hljs-keyword">return</span> 
}
</code></pre>
<p>--</p>
<p>A little bit about <strong>SQL Injection</strong>. I have very little knowledge about that so please take it with a grain of salt. Assume we have this SQL: <code>SELECT {{request.query.limit}} as limit, {{request.query.token}} as token;</code></p>
<p>Hitting this endpoint <code>/hello/world?token=test&amp;limit=(select "Id" from salesperson limit 1)</code> will not return the data from salesperson, instead, it returns as a literal string.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"success"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"response"</span>: {
    <span class="hljs-attr">"items"</span>: [
      {
        <span class="hljs-attr">"limit"</span>: <span class="hljs-string">"(select \"Id\" from salesperson limit 1)"</span>,
        <span class="hljs-attr">"token"</span>: <span class="hljs-string">"test"</span>
      }
    ],
    <span class="hljs-attr">"schema"</span>: <span class="hljs-literal">false</span>
  }
}
</code></pre>
<h3 id="heading-limitation-using-this-approach">Limitation Using This Approach</h3>
<ul>
<li><p>For a normal backend, we most likely use more than one endpoint path (command). For a single command, we will have more than one node. Managing all of that by writing directly in the Outerbase will be risky, e.g. incidentally deleting a node or command.</p>
</li>
<li><p>We can't use 3rd party library or split our code into several files</p>
</li>
<li><p>We can't reuse the same code</p>
</li>
<li><p>The template format <code>{{request.body}}</code> will break the linter/syntax highlighter/IntelliSense in VSCode or your favorite IDE</p>
</li>
</ul>
<p>These problems made me create a solution in the next section.</p>
<h2 id="heading-using-pnpm-workspace-and-vite">Using PNPM Workspace and Vite</h2>
<p>First of all, I already create a template you can work for so you don't have to prepare it yourself. Go to this Github repo <a target="_blank" href="https://github.com/tegarimansyah/outerbase-commands-workspace">outerbase-commands-workspace</a> and click <code>use this template</code>(don't forget to star 😆). The next 2 subsections will talk about how the template works internally after that we will learn how to use it in real code.</p>
<h3 id="heading-pnpm-workspace-for-multiple-command-and-nodes">PNPM Workspace for Multiple Command and Nodes</h3>
<p>I use pnpm instead of npm and yarn, and <a target="_blank" href="https://pnpm.io/workspaces">pnpm workspace</a> is how we built a monorepo (don't confuse it with monolithic). With monorepo, we can put all of our code for command and node in a single place. Interestingly, we can also share functions to be used by some nodes without maintaining another repo (for that lib). Using pnpm, we also save some storage since the library will be symlinked instead of installing node_modules for each of our nodes.</p>
<p>After using the template and cloning your repo, you can run <code>pnpm install</code> to initialize. Here is the folder structure</p>
<pre><code class="lang-bash">$ tree -L 1
.
├── commands            <span class="hljs-comment"># the source code</span>
├── dist                <span class="hljs-comment"># the build code</span>
├── node_modules
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml  <span class="hljs-comment"># define where is our source code</span>
├── README.md
└── scripts              <span class="hljs-comment"># custom bash script and templates</span>
</code></pre>
<p>And here are the available commands</p>
<pre><code class="lang-bash">$ <span class="hljs-comment"># create a new code</span>
$ pnpm run new COMMAND_NAME NODE_NAME 

$ <span class="hljs-comment"># install dependency from package.json</span>
$ pnpm --filter <span class="hljs-string">"COMMAND_NAME-NODE_NAME"</span> install

$ <span class="hljs-comment"># build into a single file </span>
$ <span class="hljs-comment"># output: COMMAND_NAME-NODE_NAME.js</span>
$ pnpm --filter <span class="hljs-string">"COMMAND_NAME-*"</span> run build 

$ <span class="hljs-comment"># build into a single file and minify (but preserve "userCode" function name)</span>
$ <span class="hljs-comment"># output: COMMAND_NAME-NODE_NAME.min.js</span>
$ pnpm --filter <span class="hljs-string">"COMMAND_NAME-*"</span> run build-minify 

$ <span class="hljs-comment"># build into a single file and minify and remove quote for {{request.*}} and {{node-*}}</span>
$ <span class="hljs-comment"># output: COMMAND_NAME-NODE_NAME.min.js.outerbase</span>
$ pnpm --filter <span class="hljs-string">"COMMAND_NAME-*"</span> run build-minify-outerbase 

$ <span class="hljs-comment"># minify an existing build that not minified yet </span>
$ pnpm run minify COMMAND_NAME-NODE_NAME <span class="hljs-comment"># without extension</span>

$ <span class="hljs-comment"># convert to outerbase compatible template (remove quote for {{request.*}} and {{node-*}}) for existing build</span>
$ pnpm run convert-to-outerbase COMMAND_NAME-NODE_NAME.js <span class="hljs-comment"># with extension</span>
</code></pre>
<p>Running <code>pnpm run xxx</code> will see in <code>package.json</code> the root and run <code>xxx</code> script. But running <code>pnpm --filter "COMMAND_NAME-*" run xxx</code> will see <code>package.json</code> in all matched project names and run <code>xxx</code> script defined there.</p>
<p>Using <code>pnpm run new</code> actually runs a bash script located in <code>scripts/new_command.sh</code> which copies <code>scripts/templates</code> folder to the <code>commands/</code> folder and make some changes for command and node names in the template. You can modify the content of the template and it will be included in your next generated code.</p>
<h3 id="heading-vite-for-spliting-files-and-use-3rd-party-lib">Vite for Spliting Files and Use 3rd Party Lib</h3>
<p>I use Vite for bundling the code (actually, rollup does the work). Let's try to create a hello world for that: <code>pnpm run new hello node-1</code>. After that, we will have a new folder in <code>command/hello-node-1</code>. Let's see what we have there.</p>
<pre><code class="lang-bash">$ tree commands/hello-node-1/

commands/hello-node-1/
├── package.json
├── src
│   ├── index.js
│   └── world.js
└── vite.config.ts

1 directory, 4 files

$ tail commands/hello-node-1/src/*

==&gt; commands/hello-node-1/src/index.js &lt;==
import world from <span class="hljs-string">"./world"</span>

<span class="hljs-built_in">export</span> <span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">userCode</span></span>() {
  console.log = <span class="hljs-string">"test"</span>
    <span class="hljs-built_in">return</span> {
      status: <span class="hljs-string">"success"</span>,
      message: `hello <span class="hljs-variable">${world()}</span>`,
      msg: <span class="hljs-string">"{{request.query.INPUT_NAME}}"</span>
    };
  }

==&gt; commands/hello-node-1/src/world.js &lt;==
<span class="hljs-built_in">export</span> default <span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">user</span></span>() { <span class="hljs-built_in">return</span> <span class="hljs-string">"Tegar"</span>}
</code></pre>
<p>In the sample code, I prepared <code>index.js</code> for our entry point that will import a local file <code>world.js</code>. You may notice that I add a quote in <code>{{request.query.INPUT_NAME}}</code>, that will be broken if we use it directly as a Outerbase node. But if we do not add the quote, it will be broken in our IDE since it's not a valid javascript syntax. So my solution is we will add the quote in our development environment and then run <code>script/outerbase_compatible.sh</code> to remove the double quote in the build file. Don't worry, I already simplified it as pnpm run script like previously I mentioned.</p>
<p>Let's add a 3rd party dependency, build it and deploy it to Outerbase. I will <code>pnpm --filter "hello*" install is-odd</code> and make some changes</p>
<pre><code class="lang-diff">import world from "./world"
<span class="hljs-addition">+ import isOdd from "is-odd"</span>

export function userCode() {
  console.log = "test"
    return {
      status: "success",
      message: `hello ${world()}`,
<span class="hljs-deletion">-     msg: "{{request.query.INPUT_NAME}}"</span>
<span class="hljs-addition">+     input: isOdd(parseInt("{{request.query.number}}"))</span>
    };
  }
</code></pre>
<p>After that, run <code>pnpm --filter "hello*" run build-minify-outerbase</code> and copy the content of <code>dist/hello-node-1.min.js.outerbase</code>. Yes, it's only a few lines and all functions will be renamed except for the <code>userCode</code>. We also removed the quote for the Outerbase template variable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695530983082/acf3b1c6-31f7-4c3b-9fc5-17529e3aa87a.png" alt class="image--center mx-auto" /></p>
<p>Paste the code to the Outerbase command and deploy it by clicking <code>Run All</code>. You can run the code by create a GET request to <code>/hello/world?number=3</code>. I usually wait until the log shows me something like this to make sure it's deployed successfully.</p>
<pre><code class="lang-bash">11:28:52:325 Starting to build Spin compatible module
11:28:52:325 Preinitiating using Wizer
11:28:54:070 Optimizing wasm binary using wasm-opt
11:29:10:357 Spin compatible module built successfully
</code></pre>
<h3 id="heading-create-more-useful-commands-and-nodes-using-this-repo">Create More Useful Commands and Nodes using This Repo</h3>
<p>Now let's try to create a node that will create an agenda for salespeople to visit several stores on specific dates. We will create a POST endpoint, validate the input and store it in the database.  </p>
<p><strong>TBA</strong></p>
<h2 id="heading-integration-with-3rd-party-services-and-add-security">Integration with 3rd Party Services and Add Security</h2>
<h3 id="heading-encrypt-secret-in-database-with-supabase-vault-use-case-to-integrate-with-aws">Encrypt Secret in Database with Supabase Vault (+ Use Case to Integrate with AWS)</h3>
<p>More about Supabase Vault can be <a target="_blank" href="https://supabase.com/blog/supabase-vault">read here</a>. In short, we encrypt our secret in the database and just in case someone gets access to our database backup, they only get the encrypted data. To receive the data, we can access a <a target="_blank" href="https://www.postgresqltutorial.com/postgresql-views/managing-postgresql-views/">VIEW TABLE</a> and get the decrypted data. Actually, we can achieve this using built-in encrypt and decrypt functions like <a target="_blank" href="https://stackoverflow.com/questions/12598382/postgresql-encrypt-decrypt">this example</a>, but I found that using Supabase Vault is easier (to create and delete the secret) and simpler (in SQL form).</p>
<p>First of all, go to your Supabase project and go to <strong>Settings -&gt; Vault -&gt; Add new secret</strong>. Here you will get a form to create a secret. For example to integrate with AWS Services:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695624608795/43a99089-2518-4755-ba12-44f64b63db30.png" alt class="image--center mx-auto" /></p>
<p>After that, we can get the secret by using this SQL</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Get from one key</span>
<span class="hljs-keyword">select</span> <span class="hljs-keyword">name</span> <span class="hljs-keyword">as</span> <span class="hljs-keyword">key</span>, decrypted_secret <span class="hljs-keyword">as</span> <span class="hljs-keyword">value</span> 
<span class="hljs-keyword">from</span> vault.decrypted_secrets <span class="hljs-keyword">where</span> <span class="hljs-keyword">name</span> = <span class="hljs-string">'AWS_ACCESS_KEY_ID'</span>;

<span class="hljs-comment">-- Get from multiple keys</span>
<span class="hljs-keyword">select</span> <span class="hljs-keyword">name</span> <span class="hljs-keyword">as</span> <span class="hljs-keyword">key</span>, decrypted_secret <span class="hljs-keyword">as</span> <span class="hljs-keyword">value</span> 
<span class="hljs-keyword">from</span> vault.decrypted_secrets <span class="hljs-keyword">where</span> <span class="hljs-keyword">name</span> <span class="hljs-keyword">in</span> 
(<span class="hljs-string">'AWS_ACCESS_KEY_ID'</span>, <span class="hljs-string">'AWS_SECRET_ACCESS_KEY'</span>);
</code></pre>
<p>The output will be something like this</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"success"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"response"</span>: {
    <span class="hljs-attr">"items"</span>: [
      {
        <span class="hljs-attr">"key"</span>: <span class="hljs-string">"AWS_ACCESS_KEY_ID"</span>,
        <span class="hljs-attr">"value"</span>: <span class="hljs-string">"AKIAX7Y4GCQPDK397AVM"</span>
      }
    ],
    <span class="hljs-attr">"schema"</span>: <span class="hljs-literal">false</span>
  }
}
</code></pre>
<blockquote>
<p>Don't use this as the last node in the command except for debugging, or else the secret will be leaked to the user</p>
</blockquote>
<p>Usually, I make a wrapper so I can access it as a key-value pair. <code>parseResult</code> is from the previous section <a target="_blank" href="https://blog.tegar.my.id/build-powerful-and-secure-multiple-outerbase-commands-with-pnpm-workspace-and-vite#heading-accessing-data-with-sql">accessing data with SQL</a>.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Node name: Get Secret</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userCode</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> secretStr = {{get-secret-node}}
    <span class="hljs-keyword">const</span> secret = parseResult(secretStr); <span class="hljs-comment">// Refer to previous section</span>
    <span class="hljs-keyword">const</span> secretKV = secret.reduce(<span class="hljs-function">(<span class="hljs-params">obj, item</span>) =&gt;</span> {
      obj[item.key] = item.value;
      <span class="hljs-keyword">return</span> obj;
    }, {});
    <span class="hljs-keyword">return</span> secretKV
}

<span class="hljs-comment">// In other node</span>
<span class="hljs-keyword">const</span> config = <span class="hljs-keyword">new</span> AWS.Config({
  <span class="hljs-attr">accessKeyId</span>: {{get-secret.AWS_ACCESS_KEY_ID}}, 
  <span class="hljs-attr">secretAccessKey</span>: {{get-secret.AWS_SECRET_ACCESS_KEY}}, 
  <span class="hljs-attr">region</span>: <span class="hljs-string">'us-east-1'</span>
});
</code></pre>
<p>The example of the AWS config is from the <a target="_blank" href="https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#constructor-property">official documentation</a>. You can use your imagination to use AWS Services.</p>
<p>While this is possible, I still prefer to add secrets in environment variables like how we usually do and comply with <a target="_blank" href="https://12factor.net/config">the 12-factor app</a>. I hope Outerbase will support this soon.</p>
<h3 id="heading-protect-endpoint-with-token-based-authentication-and-jwt-use-case-with-clerk">Protect Endpoint with T<strong>oken-Based Authentication and JWT (+ Use Case with Clerk)</strong></h3>
<p>Since Outerbase command still does not support built-in Authentication nor header and cache, then we don't have many options to make sure our endpoint can respond only to authorized requests. For this, I will use <code>token</code> key in the query string. So for each command, I will expect something like this in the first node:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Node name: "Check Auth"</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userCode</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// let's hard code the token for now</span>
    <span class="hljs-comment">// and we will modify later</span>
    <span class="hljs-keyword">if</span> ({{request.query.token}} === <span class="hljs-string">"letmein"</span>) {
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">authorized</span>: <span class="hljs-literal">true</span> }
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">authorized</span>: <span class="hljs-literal">false</span> }
    }
}
</code></pre>
<p>Unfortunately, by the time this article was written, Outerbase <a target="_blank" href="https://discord.com/channels/1123612147704934400/1153753811001810954">didn't support exiting early</a> without continuing to chain the node. So I created a small lib to check authorization and wrap the main function with it. I do this for all nodes after the <code>Check Auth</code> node. Assume you are using the template repo I provided:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// lib/checkAuth.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkAuth</span>(<span class="hljs-params">fn</span>) </span>{
    <span class="hljs-comment">// The build step will remove the quote</span>
    <span class="hljs-comment">// Make sure your auth node name is "Check Auth" or else it doesn't work</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-string">"{{check-auth.authorized}}"</span>) {
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">status</span>: <span class="hljs-string">"error"</span>, <span class="hljs-attr">code</span>: <span class="hljs-number">403</span>, <span class="hljs-attr">message</span>: <span class="hljs-string">"Unauthorized"</span> }
    <span class="hljs-keyword">else</span> {
       <span class="hljs-keyword">return</span> fn()
    }
} 

<span class="hljs-comment">// Import to main.js</span>
<span class="hljs-keyword">import</span> checkAuth <span class="hljs-keyword">from</span> <span class="hljs-string">"@/lib/checkAuth"</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// Do something heavy</span>
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userCode</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> checkAuth(main)
}
</code></pre>
<p>So later if Outerbase finally supports it, I just simply remove the import and wrapper function.</p>
<details><summary>For Skiping SQL Node</summary><div data-type="detailsContent">Honestly, I'm not experimenting with this but conditional query AFAIK is not a standard SQL. PostgreSQL does support <code>if</code> statement and I think we <a target="_blank" href="https://stackoverflow.com/questions/11299037/postgresql-if-statement">can use this</a>, but it's for procedural programming language (like you create a function that will run in postgres). I think it's wiser to wait Outerbase team to support this feature rather than go for unnecessary complexity.</div></details>

<p>Back to the token, the token checker is hard-coded and that's not ideal. We can use our previous about <a target="_blank" href="https://blog.tegar.my.id/build-powerful-and-secure-multiple-outerbase-commands-with-pnpm-workspace-and-vite#heading-encrypt-secret-in-database-with-supabase-vault">encrypt secret in database</a> and then use it. It's very simple to implement but now since the token is only one, we can't differentiate who makes a call. It's not suitable for apps that are used by multiple users.</p>
<p>And now for that scenario, we can use a different approach: <strong>JWT</strong>. We can create our own JWT using <a target="_blank" href="https://jwt.io/libraries">JWT libraries</a> from your favorite programming language. We can use Auth services like <a target="_blank" href="https://clerk.com/">Clerk</a> or <a target="_blank" href="https://auth0.com/">Auth0</a>, but in this article, we will focus on using <strong>Clerk</strong>. The idea is the user will sign in to clerk -&gt; clerk generates JWT -&gt; client side (browser) will create a request with JWT -&gt; Outerbase command in <code>Check Auth</code> node will verify whether it's a valid JWT or not.</p>
<details><summary>If you want to read more about the JWT verification method.</summary><div data-type="detailsContent">JWT consists of 3 components: header, payload and signature. To verify, we will hash base64 encoded header + base64 encoded payload + secret or Public-Private key. Using secret or Public-Private key depends on the hashing algorithm defined in the header, for example, HS256 will use secret and RS256 (<strong>Clerk uses this</strong>) will use Public-Private key. When creating a JWT, Clerk will create a header and payload, then hash both and sign with it's private key as a signature. We don't need the private key, but need the public key to verify whether the JWT is tampered with or not. Sure we can decode the payload to see the content without verifying it, but that will be a serious security issue.</div></details>

<p>Normally, we will retrieve the JWT from the request header or session. But for our approach now, we will expect the client to send the JWT via query string.</p>
<p>For getting JWT from Clerk, you can see the code I use from stackblitz here. (Refresh the stackblitz if shows error)</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://stackblitz.com/edit/web-platform-wasx22?file=main.js">https://stackblitz.com/edit/web-platform-wasx22?file=main.js</a></div>
<p> </p>
<p>For verifying JWT, you can refer to the Clerk documentation about <a target="_blank" href="https://clerk.com/docs/backend-requests/handling/manual-jwt">manual verification</a>. This is a modified version that is simplified for our use case. Since this needs 3rd party library, you may need <a target="_blank" href="https://github.com/tegarimansyah/outerbase-commands-workspace">the build tool</a>.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Node name: "Check Auth"</span>
<span class="hljs-keyword">import</span> jwt <span class="hljs-keyword">from</span> <span class="hljs-string">"jsonwebtoken"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userCode</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> token = <span class="hljs-string">"{{request.query.token}}"</span>

    <span class="hljs-comment">// Get from API Key -&gt; Advanced -&gt; JWT Public Key -&gt; PEM</span>
    <span class="hljs-keyword">const</span> publicKey = <span class="hljs-string">`-----BEGIN PUBLIC KEY-----
    ....
    -----END PUBLIC KEY-----`</span>;
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> decoded = jwt.verify(token, publicKey);
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">authorized</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">sessToken</span>: decoded }
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">authorized</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">"Invalid Token"</span> }
    }
}
</code></pre>
<p>Too complicated? Actually, you can use <a target="_blank" href="https://clerk.com/docs/reference/backend-api/tag/Clients#operation/VerifyClient">this API</a> from Clerk to verify the token from the server side. But I'm not a fan of this method since it needs time to call API (and another time if you also retrieve secret token for Clerk from the database secret)</p>
<h3 id="heading-scheduling-job-to-remind-sales-person-with-upstash">Scheduling Job to Remind Sales Person with Upstash</h3>
<p><strong>TBA</strong></p>
<h3 id="heading-add-push-notification-if-a-deal-is-closed-with-ntfy">Add Push Notification If a Deal is Closed with Ntfy</h3>
<p><strong>TBA</strong></p>
<h3 id="heading-automatic-monthly-report-send-to-email-with-mailgun">Automatic Monthly Report Send to Email with Mailgun</h3>
<p><strong>TBA</strong></p>
<h3 id="heading-app-and-audit-log-for-debugging-with-axiom">App and Audit Log for Debugging with Axiom</h3>
<p><strong>TBA</strong></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>That's all my exploration with Outerbase command. It's not a complete app yet, in the next post we will see how I created a dashboard and plugin to make my father oversee his salesperson.</p>
<p>Thank you <a target="_blank" href="https://hashnode.com/">Hashnode</a> and <a target="_blank" href="https://outerbase.com/">Outerbase</a> for hosting a <a target="_blank" href="https://hashnode.com/hackathons/outerbase">Hackathon</a>. That makes me accelerate my development process to finish this dormant idea.</p>
<p>I usually talk about rapid development and love to review Platform as a Service that helps developers' lives easier. Connect with me on <a target="_blank" href="https://twitter.com/tegar_imansyah">Twitter</a> and <a target="_blank" href="https://www.linkedin.com/in/tegarimansyah/">Linkedin</a> to talk more about that.</p>
]]></content:encoded></item><item><title><![CDATA[Running Knative with KinD (Kubernetes in Docker) in Macbook Air M1]]></title><description><![CDATA[One of my clients is building an AI app running on top of a Kubernetes cluster with GPU. It's been more than a year I'm not touching anything about Kubernetes in production. What makes me interested is how the app is running: it's using Kubeflow's In...]]></description><link>https://blog.tegar.my.id/running-knative-with-kind-kubernetes-in-docker-in-macbook-air-m1</link><guid isPermaLink="true">https://blog.tegar.my.id/running-knative-with-kind-kubernetes-in-docker-in-macbook-air-m1</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[Docker]]></category><category><![CDATA[macOS]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Tegar Imansyah]]></dc:creator><pubDate>Tue, 12 Sep 2023 13:28:49 GMT</pubDate><content:encoded><![CDATA[<p>One of my clients is building an AI app running on top of a Kubernetes cluster with GPU. It's been more than a year I'm not touching anything about Kubernetes in production. What makes me interested is how the app is running: it's using Kubeflow's InferenceService, which is running on top of Knative.</p>
<p>Before we start, you need to install:</p>
<ul>
<li><p><a target="_blank" href="https://docs.docker.com/desktop/install/mac-install/">Install Docker</a></p>
</li>
<li><p><a target="_blank" href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation">Install KinD</a>: <code>brew install kind</code></p>
</li>
</ul>
<h2 id="heading-tldr-copy-this-script">TLDR: Copy This Script</h2>
<p>The script below will create kind cluster, install knative, run the hello world app and auto-scaling app.</p>
<pre><code class="lang-bash">brew install knative/client/kn
brew install knative-sandbox/kn-plugins/quickstart
kn quickstart kind --registry <span class="hljs-comment"># this will also install kind local registry</span>
kn service create hello --image ghcr.io/knative/helloworld-go:latest --port 8080 --env TARGET=World
kubectl apply -f https://raw.githubusercontent.com/knative/docs/main/docs/serving/autoscaling/autoscale-go/service.yaml
</code></pre>
<h2 id="heading-so-what-happened">So, What Happened?</h2>
<p>Check if everything is good</p>
<pre><code class="lang-diff">$ docker ps
CONTAINER ID   IMAGE                  COMMAND                  CREATED       STATUS       PORTS                                              NAMES
a67f84478882   registry:2             "/entrypoint.sh /etc…"   6 hours ago   Up 6 hours   0.0.0.0:5001-&gt;5000/tcp                             kind-registry
40cb8f208db6   kindest/node:v1.26.6   "/usr/local/bin/entr…"   6 hours ago   Up 6 hours   127.0.0.1:60178-&gt;6443/tcp, 0.0.0.0:80-&gt;31080/tcp   knative-control-plane

$ docker stats
CONTAINER ID   NAME                    CPU %     MEM USAGE / LIMIT     MEM %     NET I/O          BLOCK I/O        PIDS
a67f84478882   kind-registry           0.10%     5.336MiB / 1.942GiB   0.27%     1.57kB / 0B      752MB / 24.6MB   6
40cb8f208db6   knative-control-plane   40.89%    1.225GiB / 1.942GiB   63.06%    279MB / 16.8MB   229GB / 7.53GB   581

$ kind get clusters # will show `knative` cluster
knative

$ kubectl get namespace # will show default namespace and knative namespace
NAME                 STATUS   AGE
default              Active   105m
<span class="hljs-addition">+ knative-eventing     Active   101m</span>
<span class="hljs-addition">+ knative-serving      Active   103m</span>
<span class="hljs-addition">+ kourier-system       Active   102m</span>
kube-node-lease      Active   105m
kube-public          Active   105m
kube-system          Active   105m
local-path-storage   Active   105m

$ kubectl get ksvc # it's ksvc, not svc. Will show knative services
NAME           URL                                              LATESTCREATED        LATESTREADY          READY   REASON
autoscale-go   http://autoscale-go.default.127.0.0.1.sslip.io   autoscale-go-00001   autoscale-go-00001   True
hello          http://hello.default.127.0.0.1.sslip.io          hello-00001          hello-00001          True

$ kubectl get deployment
NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
autoscale-go-00001-deployment   0/0     0            0           151m
hello-00001-deployment          0/0     0            0           4h16m
</code></pre>
<p>We can see that our first script will create a KinD cluster called <code>knative</code>. KinD is Kubernetes in Docker, so we can see the cluster and its status with docker command. But every container that runs inside kubernetes will be invisible in docker.</p>
<p>We have knative serving (that will response to HTTP request), knative eventing (that will response to event aka event-driven app) and <a target="_blank" href="https://developers.redhat.com/blog/2020/06/30/kourier-a-lightweight-knative-serving-ingress">kourier</a> (for networking, previously knative use istio).</p>
<p>To see our running apps, we can see via <code>k get ksvc</code> aka knative service. Open the URL and you will get the response instantly (or you may get cold start). The interesting part is knative also creates a deployment and the replica is dynamic, if no traffic for 2 minutes (by default), then it will scale to zero. Give it a single traffic and it will change the replica to 1.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694513556387/ccf53943-c86c-42b1-a2c0-d093733c5406.png" alt class="image--center mx-auto" /></p>
<p>The first request will suffer a cold start, after that, we can get a more quick response. Let's look at a glance at the manifest for <code>autoscale-go</code></p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">serving.knative.dev/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">autoscale-go</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">annotations:</span>
        <span class="hljs-comment"># Target 10 in-flight-requests per pod.</span>
        <span class="hljs-attr">autoscaling.knative.dev/target:</span> <span class="hljs-string">"10"</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">image:</span> <span class="hljs-string">ghcr.io/knative/autoscale-go:latest</span>
</code></pre>
<p>It's a simple manifest that handles everything behind the scenes. We will have a deep talk about it in another post.</p>
<h2 id="heading-performance-test-to-trigger-autoscaling">Performance Test to Trigger Autoscaling</h2>
<p>Scale from 0 to 1 is cool, but 1 pod can't handle anything serious. So we will create some traffic with <a target="_blank" href="https://k6.io/">K6 performance tools</a>. But before that, let's get the URL for app called <code>autoscale-go</code></p>
<pre><code class="lang-bash">$ <span class="hljs-comment"># Get URL for autoscale-go</span>
$ kubectl get ksvc 
$ <span class="hljs-comment"># Monitor if any update for our pod. You can change pod to deployment</span>
$ kubectl get pod -l serving.knative.dev/service=autoscale-go -w
</code></pre>
<p>Copy the code and save it as <code>perftest.js</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { sleep } <span class="hljs-keyword">from</span> <span class="hljs-string">'k6'</span>;
<span class="hljs-keyword">import</span> http <span class="hljs-keyword">from</span> <span class="hljs-string">'k6/http'</span>;

<span class="hljs-comment">// Each virtual user will run this function</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-comment">// Change the url </span>
  <span class="hljs-keyword">const</span> appURL = <span class="hljs-string">"http://autoscale-go.default.127.0.0.1.sslip.io/?sleep=50&amp;prime=10000&amp;bloat=5"</span>
  http.get(appURL);

  <span class="hljs-comment">// sleep for 0.1 second before finishing the task</span>
  sleep(<span class="hljs-number">0.1</span>) 
}
</code></pre>
<p>Lastly, open new terminal and run k6</p>
<pre><code class="lang-bash">$ <span class="hljs-comment"># Create and maintain 50 virtual users and run the code for 30s</span>
$ k6 run --vus 50 --duration 30s perftest.js
</code></pre>
<p>Let's monitor how it will behave</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694516867051/f3be64e3-21fa-4cc1-9a4b-cf0f028d3e17.png" alt class="image--center mx-auto" /></p>
<p>After kicking the performance test, the deployment replica jumps from 1 to 7 (top left) and we can see how the lifecycle of the pod (top right). The cpu and memory usage of KinD cluster also jump from 30% and 60% to 282% and 81% respectively. It's good because we don't need to set anything funny to set up this simple app.</p>
<p>And how about the result?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694517190902/e1e30819-1fca-4c6d-9b73-2a2fe0978e66.png" alt class="image--center mx-auto" /></p>
<p>For simplicity, we will only see the <code>http_req_failed</code>, <code>http_req_duration</code> and <code>iterations</code>. I think we already saturated the cluster since we only got 368 completed requests with an average 4.31s and p90 9.88s, fortunately, all requests have been served successfully. I tried it several times with different vus (e.g. 10, 20, and so on) and it got more completed requests with faster response time.</p>
<h2 id="heading-knative-serverless-is-for-stateless-app">Knative Serverless is for Stateless App</h2>
<p><a target="_blank" href="https://youtu.be/0IwysONytqc?feature=shared&amp;t=1793"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694524994469/f9593d67-31e2-4fdb-9216-4f7b3304f923.png" alt class="image--center mx-auto" /></a></p>
<p>Or watch full video:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/0IwysONytqc?feature=shared&amp;t=1793">https://youtu.be/0IwysONytqc?feature=shared&amp;t=1793</a></div>
<p> </p>
<p>Before you follow the hype, I should tell you this: Knative will be best if your app is stateless. If you have a vague idea about stateless, please read more about <a target="_blank" href="https://12factor.net/">12-factor app</a>. TLDR, It's a guideline to make our app stateless, which means our app doesn't store any data (e.g. file, session, persistent data). Therefore, any data will be stored in a stateful backing service (e.g. postgres, redis). If you want to use some framework that use state by default (e.g. odoo, magento, etc), you should make some changes to be able to use it in Knative (or Kubernetes and docker in common).</p>
<h2 id="heading-what-next">What Next?</h2>
<p>My goal is to understand this new way to deploy apps to Kubernetes. In this article, we discover a simple app that is far from production. In the future we will look how to implement more complex API, add environment variables and secret, add monitoring and logging, do rolling deployment, and ultimately implement it together with kubeflow for AI app.</p>
]]></content:encoded></item></channel></rss>