Merge branch 'main' into tailwind-integration

This commit is contained in:
Jesse Winton
2024-08-26 09:04:09 -04:00
28 changed files with 1469 additions and 730 deletions

3
.gitignore vendored
View File

@@ -16,3 +16,6 @@ package-lock.json
# Sentry Config File
.sentryclirc
.history
terraform/**/.t*
terraform/**/.env
terraform/**/**/*.tfstate*

View File

@@ -51,7 +51,7 @@ APIs section:
Tooling section:
- [Command Line](https://appwrite.io/docs/command-line)
- [CLI](https://appwrite.io/docs/command-line)
- [Command center](https://appwrite.io/docs/tooling/command-center)
- [Assistant](https://appwrite.io/docs/tooling/assistant)

View File

@@ -10,6 +10,7 @@
body: JSON.stringify({
name,
email,
cloud: true /* not optional on the growth endpoint. */,
}),
},
);

View File

@@ -1,5 +1,5 @@
export const GITHUB_STARS = '42.8K';
export const BANNER_KEY: Banners = 'init-banner-02'; // Change key to force banner to show again
export const BANNER_KEY: Banners = 'discord-banner-01'; // Change key to force banner to show again
export const SENTRY_DSN =
'https://27d41dc8bb67b596f137924ab8599e59@o1063647.ingest.us.sentry.io/4507497727000576';

View File

@@ -29,6 +29,7 @@
export let timeToRead: string;
export let cover: string;
export let category: string;
export let lastUpdated: string;
const authors = getContext<AuthorData[]>("authors");
const authorData = authors.find((a) => a.slug === author);
@@ -246,12 +247,21 @@
</div>
{/if}
<div class="web-article-content mt-8">
<div class="web-article-content u-margin-block-start-32">
{#if lastUpdated}
<span class="web-main-body-500 last-updated-text">
Updated:
<time dateTime={lastUpdated}>
{formatDate(lastUpdated)}
</time>
</span>
{/if}
<slot />
</div>
</article>
<!-- {#if categories?.length}
<div class="flex gap-4">
<div class="u-flex u-gap-16">
{#each categories as cat}
<a href={cat.href} class="web-tag">{cat.name}</a>
{/each}
@@ -330,5 +340,9 @@
.web-icon-copy {
font-size: 24px;
}
.last-updated-text {
color: var(--primary, #e4e4e7);
}
}
</style>

View File

@@ -22,6 +22,7 @@ export function load() {
description: frontmatter.description,
featured: frontmatter?.featured ?? false,
date: new Date(frontmatter.date),
lastUpdated: new Date(frontmatter.lastUpdated),
cover: frontmatter.cover,
timeToRead: frontmatter.timeToRead,
author: frontmatter.author,

View File

@@ -20,6 +20,7 @@ export type PostsData = {
title: string;
description: string;
date: Date;
lastUpdated: Date;
cover: string;
timeToRead: number;
author: string;
@@ -51,6 +52,7 @@ export const posts = Object.entries(postsGlob)
title: frontmatter.title,
description: frontmatter.description,
date: new Date(frontmatter.date),
lastUpdated: new Date(frontmatter.lastUpdated),
cover: frontmatter.cover,
timeToRead: frontmatter.timeToRead,
author: frontmatter.author,

View File

@@ -3,6 +3,7 @@ layout: post
title: "Announcing: A new Init. Faster. Smoother. Better."
description: Init is about to begin, and were bringing you many new features and products to be excited about. Experience everything new with Appwrite during the week of August 19 to 23.
date: 2024-08-07
lastUpdated: 2024-08-10
cover: /images/blog/announcing-init-faster-smoother-better/init-cover.png
timeToRead: 4
author: eldad-fux

View File

@@ -97,7 +97,7 @@
label: 'Tooling',
items: [
{
label: 'Command Line',
label: 'CLI',
href: '/docs/tooling/command-line/installation',
icon: 'icon-terminal',
isParent: true

View File

@@ -40,7 +40,7 @@ Deligate access for a user through passing JWT tokens.
{% cards_item href="/docs/products/auth/server-side-rendering" title="Server-side rendering (SSR)" %}
Authenticate users in server-side rendered applications.
{% /cards_item %}
{% cards_item href="/docs/products/auth/custom-token" title="Custom Token" %}
{% cards_item href="/docs/products/auth/custom-token" title="Custom token" %}
Implement custom authentication methods like biometric and passkey login by generating custom tokens.
{% /cards_item %}
{% cards_item href="/docs/products/auth/mfa" title="Multifactor authentication (MFA)" %}

View File

@@ -206,6 +206,54 @@ export default ({ req, res, log, error }: any) => {
});
};
```
```go
package handler
import (
"fmt"
"os"
"github.com/appwrite/sdk-for-go/appwrite"
"github.com/open-runtimes/types-for-go/v4/openruntimes"
)
type Response struct {
Motto string `json:"motto"`
Learn string `json:"learn"`
Connect string `json:"connect"`
GetInspired string `json:"getInspired"`
}
func Main(Context openruntimes.Context) openruntimes.Response {
// This is your Appwrite function
// It's executed each time we get a request service
var _ = appwrite.NewClient(
appwrite.WithProject(os.Getenv("APPWRITE_FUNCTION_PROJECT_ID")),
appwrite.WithKey(Context.Req.Headers["x-appwrite-key"]),
)
// You can log messages to the console
fmt.Println("Hello, Logs!")
fmt.Fprintln(os.Stderr, "Error:", "Hello, Errors!")
// The `Context.Req` object contains the request data
if Context.Req.Method == "GET" {
// Send a response with the Context.Res object helpers
// `Context.Res.Text()` dispatches a string back to the client
return Context.Res.Text("Hello, World!")
}
// `res.json()` is a handy helper for sending JSON
return Context.Res.Json(
Response{
Motto: "Build like a team of hundreds_",
Learn: "https://appwrite.io/docs",
Connect: "https://appwrite.io/discord",
GetInspired: "https://builtwith.appwrite.io",
})
}
```
```dart
import 'dart:async';
import 'package:dart_appwrite/dart_appwrite.dart';
@@ -437,7 +485,7 @@ You'll see us use destructuring in examples, which has the following syntax.
[Learn more about destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment).
{% multicode %}
```js
```server-nodejs
// before destructuring
export default async function (context) {
context.log("This is a log!");
@@ -477,9 +525,6 @@ Explore the request object with the following function, which logs all request p
* Request
* Description
---
* `req.bodyRaw`
* Returns the raw body text data.
---
* `req.bodyText`
* Returns text that has been converted from binary data.
---
@@ -492,9 +537,9 @@ Explore the request object with the following function, which logs all request p
{% /table %}
{% multicode %}
```js
```server-nodejs
export default async ({ req, res, log }) => {
log(req.bodyJson); // Raw request body, contains request data
log(req.bodyText); // Raw request body, contains request data
log(JSON.stringify(req.bodyJson)); // Object from parsed JSON request body, otherwise string
log(JSON.stringify(req.headers)); // String key-value pairs of all request headers, keys are lowercase
log(req.scheme); // Value of the x-forwarded-proto header, usually http or https
@@ -512,8 +557,7 @@ export default async ({ req, res, log }) => {
```php
<?php
return function ($context) {
$context->log($context->req->bodyRaw); // Raw request body, contains request data
$context->log(json_encode($context->req->body)); // Object from parsed JSON request body, otherwise string
$context->log(json_encode($context->req->bodyJson));// Object from parsed JSON request body, otherwise string
$context->log(json_encode($context->req->headers)); // String key-value pairs of all request headers, keys are lowercase
$context->log($context->req->scheme); // Value of the x-forwarded-proto header, usually http or https
$context->log($context->req->method); // Request method, such as GET, POST, PUT, DELETE, PATCH, etc.
@@ -531,7 +575,7 @@ return function ($context) {
import json
def main(context):
context.log(context.req.body_raw) # Raw request body, contains request data
context.log(context.req.bodyText) # Raw request body, contains request data
context.log(json.dumps(context.req.bodyJson)) # Object from parsed JSON request body, otherwise string
context.log(json.dumps(context.req.headers)) # String key-value pairs of all request headers, keys are lowercase
context.log(context.req.scheme) # Value of the x-forwarded-proto header, usually http or https
@@ -549,7 +593,7 @@ def main(context):
require 'json'
def main(context)
context.log(context.req.bodyJson) # Raw request body, contains request data
context.log(context.req.bodyText) # Raw request body, contains request data
context.log(JSON.generate(context.req.bodyJson)) # Object from parsed JSON request body, otherwise string
context.log(JSON.generate(context.req.headers)) # String key-value pairs of all request headers, keys are lowercase
context.log(context.req.scheme) # Value of the x-forwarded-proto header, usually http or https
@@ -566,7 +610,7 @@ end
```
```deno
export default async ({ req, res, log }: any) => {
log(req.bodyJson); // Raw request body, contains request data
log(req.bodyText); // Raw request body, contains request data
log(JSON.stringify(req.bodyJson)); // Object from parsed JSON request body, otherwise string
log(JSON.stringify(req.headers)); // String key-value pairs of all request headers, keys are lowercase
log(req.scheme); // Value of the x-forwarded-proto header, usually http or https
@@ -581,12 +625,37 @@ export default async ({ req, res, log }: any) => {
return res.text("All the request parameters are logged to the Appwrite Console.");
}
```
```go
package handler
import (
"encoding/json"
"github.com/open-runtimes/types-for-go/v4/openruntimes"
)
func Main(Context openruntimes.Context) openruntimes.Response {
Context.Log(Context.Req.BodyText) // Raw request body, contains request data
Context.Log(json.Marshal(Context.Req.BodyJson)) // Object from parsed JSON request body, otherwise string
Context.Log(json.Marshal(Context.Req.Headers)) // String key-value pairs of all request headers, keys are lowercase
Context.Log(Context.Req.Scheme) // Value of the x-forwarded-proto header, usually http or https
Context.Log(Context.Req.Method) // Request method, such as GET, POST, PUT, DELETE, PATCH, etc.
Context.Log(Context.Req.Url) // Full URL, for example: http://awesome.appwrite.io:8000/v1/hooks?limit=12&offset=50
Context.Log(Context.Req.Host) // Hostname from the host header, such as awesome.appwrite.io
Context.Log(Context.Req.Port) // Port from the host header, for example 8000
Context.Log(Context.Req.Path) // Path part of URL, for example /v1/hooks
Context.Log(Context.Req.QueryString) // Raw query params string. For example "limit=12&offset=50"
Context.Log(json.Marshal(Context.Req.Query)) // Parsed query params. For example, req.query.limit
return Context.Res.Text("All the request parameters are logged to the Appwrite Console.")
}
```
```dart
import 'dart:async';
import 'dart:convert';
Future<dynamic> main(final context) async {
context.log(context.req.bodyJson); // Raw request body, contains request data
context.log(context.req.bodyText); // Raw request body, contains request data
context.log(json.encode(context.req.bodyJson)); // Object from parsed JSON request body, otherwise string
context.log(json.encode(context.req.headers)); // String key-value pairs of all request headers, keys are lowercase
context.log(context.req.scheme); // Value of the x-forwarded-proto header, usually http or https
@@ -629,8 +698,7 @@ using System.Text.Json;
public class Handler {
public async Task<RuntimeOutput> Main(RuntimeContext Context)
{
Context.Log(Context.Req.BodyRaw); // Raw request body, contains request data
Context.Log(JsonSerializer.Serialize<object>(Context.Req.Body)); // Object from parsed JSON request body, otherwise string
Context.Log(JsonSerializer.Serialize<object>(Context.Req.BodyJson)); // Object from parsed JSON request body, otherwise string
Context.Log(JsonSerializer.Serialize<object>(Context.Req.Headers)); // String key-value pairs of all request headers, keys are lowercase
Context.Log(Context.Req.Scheme); // Value of the x-forwarded-proto header, usually http or https
Context.Log(Context.Req.Method); // Request method, such as GET, POST, PUT, DELETE, PATCH, etc.
@@ -683,7 +751,6 @@ public class Main {
public RuntimeOutput main(RuntimeContext context) {
Gson gson = new Gson();
context.log(context.getReq().getBodyRaw()); // Raw request body, contains request data
context.log(gson.toString(context.getReq().getBody())); // Object from parsed JSON request body, otherwise string
context.log(gson.toString(context.getReq().getHeaders())); // String key-value pairs of all request headers, keys are lowercase
context.log(context.getReq().getScheme()); // Value of the x-forwarded-proto header, usually http or https
@@ -718,7 +785,7 @@ These are provided alongside any custom headers sent to the function.
| `x-appwrite-continent-eu` | Describes if the configured local is within the EU. |
## Response {% #response %}
If you need to send a response to the invoker of the function, such as a user, client app, or an integration, use the response object.
Use the response object to send a response to the function caller. This could be a user, client app, or an integration.
The response information **will not be logged** to the Appwrite Console.
There are several possible ways to send a response, explore them in the following Appwrite Function.
@@ -745,7 +812,9 @@ There are several possible ways to send a response, explore them in the followin
{% /table %}
{% multicode %}
```js
```server-nodejs
const fs = require('fs');
export default async ({ req, res, log }) => {
switch (req.query.type) {
@@ -753,6 +822,8 @@ export default async ({ req, res, log }) => {
return res.empty();
case 'json':
return res.json({"type": "This is a JSON response"});
case 'binary':
return res.binary(InputFile.fromPath('/path/to/file', 'filename'));
case 'redirect':
return res.redirect("https://appwrite.io", 301);
case 'html':
@@ -767,6 +838,7 @@ export default async ({ req, res, log }) => {
```
```php
<?php
use Appwrite\InputFile;
return function ($context) {
switch ($context->req->query['type']) {
@@ -774,6 +846,8 @@ return function ($context) {
return $context->res->empty();
case 'json':
return $context->res->json(["type" => "This is a JSON response"]);
case 'binary':
return $context->res->binary(InputFile::withPath('file.png'));
case 'redirect':
return $context->res->redirect("https://appwrite.io", 301);
case 'html':
@@ -786,6 +860,8 @@ return function ($context) {
};
```
```python
from appwrite.input_file import InputFile
def main(context):
type = context.req.query['type']
@@ -793,6 +869,8 @@ def main(context):
return context.res.empty()
elif type == 'json':
return context.res.json({"type": "This is a JSON response"})
elif type == 'binary':
return context.res.binary(InputFile.from_path('file.png'))
elif type == 'redirect':
return context.res.redirect("https://appwrite.io", 301)
elif type == 'html':
@@ -809,6 +887,8 @@ def main(context)
return context.res.empty()
when 'json'
return context.res.json({"type": "This is a JSON response"})
when 'binary'
return context.res.binary(InputFile.from_path('dir/file.png'))
when 'redirect'
return context.res.redirect("https://appwrite.io", 301)
when 'html'
@@ -828,6 +908,8 @@ export default async ({ req, res, log }) => {
return res.empty();
case 'json':
return res.json({type: "This is a JSON response"});
case 'binary':
return res.binary(InputFile.fromPath('/path/to/file.png', 'file.png'));
case 'redirect':
return res.redirect("https://appwrite.io", 301);
case 'html':
@@ -840,6 +922,35 @@ export default async ({ req, res, log }) => {
}
}
```
```go
package handler
import (
"io"
"os"
"github.com/open-runtimes/types-for-go/v4/openruntimes"
)
func Main(Context openruntimes.Context) openruntimes.Response {
switch Context.Req.Query["type"] {
case "empty":
return Context.Res.Empty()
case "json":
return Context.Res.Json(map[string]string{"type": "This is a JSON response"})
case "binary":
file, _ := os.Open("./destiny.png")
imageData, _ := io.ReadAll(file)
return Context.Res.Binary(imageData)
case "redirect":
return Context.Res.Redirect("https://appwrite.io")
case "html":
return Context.Res.Text("<h1>This is an HTML response</h1>")
default:
return Context.Res.Text("This is a text response")
}
}
```
```dart
import 'dart:async';
@@ -849,6 +960,8 @@ Future<dynamic> main(final context) async {
return context.res.empty();
case 'json':
return context.res.json({'type': 'This is a JSON response'});
case 'binary':
return context.res.binary(InputFile(path: './path-to-files/image.jpg', filename: 'image.jpg'));
case 'redirect':
return context.res.redirect('https://appwrite.io', 301);
case 'html':
@@ -868,6 +981,8 @@ func main(context: RuntimeContext) async throws -> RuntimeOutput {
return context.res.empty()
case "json":
return context.res.text(["type": "This is a JSON response"])
case "binary":
return context.res.binary(InputFile.fromPath("file.png"))
case "redirect":
return context.res.redirect("https://appwrite.io", 301)
case "html":
@@ -889,6 +1004,8 @@ public class Handler {
return Context.Res.Empty();
case "json":
return Context.Res.Text(new Dictionary<string, object>() { { "type", "This is a JSON response" } });
case "binary":
return Context.Res.Binary(InputFile.FromPath("./path-to-files/image.jpg"));
case "redirect":
return Context.Res.Redirect("https://appwrite.io", 301);
case "html":
@@ -906,12 +1023,14 @@ package io.openruntimes.kotlin.src
import io.openruntimes.kotlin.RuntimeContext
import io.openruntimes.kotlin.RuntimeOutput
import io.appwrite.models.InputFile
class Main {
fun main(context: RuntimeContext): RuntimeOutput {
when (context.req.query["type"]) {
"empty" -> return context.res.empty()
"json" -> return context.res.text(mapOf("type" to "This is a JSON response"))
"binary" -> return context.res.binary(InputFile.fromPath("file.png"))
"redirect" -> return context.res.redirect("https://appwrite.io", 301)
"html" -> return context.res.text("<h1>This is an HTML response</h1>", 200, mapOf("content-type" to "text/html"))
else -> return context.res.text("This is a text response")
@@ -924,6 +1043,7 @@ package io.openruntimes.java.src;
import io.openruntimes.java.RuntimeContext;
import io.openruntimes.java.RuntimeOutput;
import io.appwrite.models.InputFile;
import java.util.Map;
import java.util.HashMap;
@@ -936,6 +1056,8 @@ public class Main {
HashMap<String, Object> data = new HashMap<>();
data.put("type", "This is a JSON response");
return context.getRes().text(data);
case "binary"
return context.getRes().binary(InputFile.fromPath("file.png"));
case "redirect":
return context.getRes().redirect("https://appwrite.io", 301);
case "html":
@@ -964,6 +1086,8 @@ namespace runtime {
Json::Value data;
data["type"] = "This is a JSON response";
return context.res.text(data);
} else if (type == "binary") {
return context.res.binary(InputFile.fromPath("file.png"))
} else if (type == "redirect") {
return context.res.redirect("https://appwrite.io", 301);
} else if (type == "html") {
@@ -991,17 +1115,22 @@ To get the different response types, set one of the following query parameters i
## Logging {% #logging %}
To protect user privacy, the request and response objects are not logged to the Appwrite Console by default.
We support the spread operator across most of the languages, meaning you can write code that is more concise and flexible.
This means, to see logs or debug function executions you need to use the `log()` and `error()` methods.
These logs are only visible to developers with access to the Appwrite Console.
Here's an example of using logs and errors.
{% multicode %}
```js
```server-nodejs
export default async ({ req, res, log, error }) => {
log("This is a log, use for logging information to console");
const message = "This is a log, use for logging information to console";
log("Message: ", message);
log(`This function was called with ${req.method} method`);
error("This is an error, use for logging errors to console");
const errorMessage = "This is an error, use for logging errors to console"
error("Error: ", errorMessage);
return res.text("Check the Appwrite Console to see logs and errors!");
};
@@ -1010,46 +1139,75 @@ export default async ({ req, res, log, error }) => {
<?php
return function ($context) {
$context->log("This is a log, use for logging information to console");
$message = "This is a log, use for logging information to console";
$context->log("Message: ", message);
$context->log("This function was called with " . $context->req->method . " method");
$context->error("This is an error, use for logging errors to console");
$errorMessage = "Check the Appwrite Console to see logs and errors!"
$context->error("Error: ", errorMessage);
return $context->text("Check the Appwrite Console to see logs and errors!");
};
```
```python
def main(context):
context.log("This is a log, use for logging information to console")
message = "This is a log, use for logging information to console"
context.log("Message: ", message)
context.log(f"This function was called with {context.req.method} method")
context.error("This is an error, use for logging errors to console")
errorMessage = "This is an error, use for logging errors to console"
context.error("Error: ", errorMessage)
return context.res.text("Check the Appwrite Console to see logs and errors!")
```
```ruby
def main(context)
context.log("This is a log, use for logging information to console")
message = "This is a log, use for logging information to console"
context.log("Message: ", message)
context.log("This function was called with #{context.req.method} method")
context.error("This is an error, use for logging errors to console")
errorMessage = "This is an error, use for logging errors to console"
context.error("Error: ", errorMessage)
return context.res.text("Check the Appwrite Console to see logs and errors!")
end
```
```deno
export default async ({ res, log, error }: any) => {
log("This is a log, use for logging information to console");
let message = "This is a log, use for logging information to console";
log("Message: ", message);
log(`This function was called with ${context.req.method} method`);
error("This is an error, use for logging errors to console");
let errorMessage = "This is an error, use for logging errors to console";
error("Error: ", errorMessage);
return res.text("Check the Appwrite Console to see logs and errors!");
};
```
```go
package handler
import (
"fmt"
"github.com/open-runtimes/types-for-go/v4/openruntimes"
)
func Main(Context openruntimes.Context) openruntimes.Response {
message := "This is a log, use for logging information to console"
Context.Log("Message: ", message)
Context.Log(fmt.Sprintf("This function was called with %s method", Context.Req.Method))
errorMessage := "This is an error, use for logging errors to console"
Context.Error("Error: ", errorMessage)
return Context.Res.Text("Check the Appwrite Console to see logs and errors!")
}
```
```dart
import 'dart:async';
Future<dynamic> main(final context) async {
context.log("This is a log, use for logging information to console");
var message = "This is a log, use for logging information to console";
context.log("message: ", var);
context.log("This function was called with ${context.req.method} method");
context.error("This is an error, use for logging errors to console");
var errorMessage = "This is an error, use for logging errors to console";
context.error("Error: ", errorMessage);
return context.res.text("Check the Appwrite Console to see logs and errors!");
}
@@ -1058,9 +1216,11 @@ Future<dynamic> main(final context) async {
import Foundation
func main(context: RuntimeContext) async throws -> RuntimeOutput {
context.log("This is a log, use for logging information to console")
var message: String = "This is a log, use for logging information to console"
context.log("Message: ", message)
context.log("This function was called with \(context.req.method) method")
context.error("This is an error, use for logging errors to console")
var message: String = "This is an error, use for logging errors to console"
context.error("Error: ", message)
return context.res.text("Check the Appwrite Console to see logs and errors!")
}
@@ -1071,9 +1231,11 @@ namespace DotNetRuntime;
public class Handler {
public async Task<RuntimeOutput> Main(RuntimeContext Context)
{
Context.Log("This is a log, use for logging information to console");
string message = "This is a log, use for logging information to console";
Context.Log("Message: ", message);
Context.Log($"This function was called with {Context.Req.Method} method");
Context.Error("This is an error, use for logging errors to console");
string errorMessage = "This is an error, use for logging errors to console";
Context.Error("Error: ", errorMessage);
return Context.Res.Text("Check the Appwrite Console to see logs and errors!");
}
@@ -1087,9 +1249,11 @@ import io.openruntimes.kotlin.RuntimeOutput
class Main {
fun main(context: RuntimeContext): RuntimeOutput {
context.log("This is a log, use for logging information to console")
var message: String = "This is a log, use for logging information to console"
context.log("Message: ", message)
context.log("This function was called with ${context.req.method} method")
context.error("This is an error, use for logging errors to console")
var errorMessage: String = "This is an error, use for logging errors to console"
context.error("Error: ", errorMessage)
return context.res.text("Check the Appwrite Console to see logs and errors!")
}
@@ -1103,9 +1267,11 @@ import io.openruntimes.java.RuntimeOutput;
public class Main {
public RuntimeOutput main(RuntimeContext context) throws Exception {
context.log("This is a log, use for logging information to console");
String message = "This is a log, use for logging information to console";
context.log("Message: ", message);
context.log("This function was called with " + context.req.method + " method");
context.error("This is an error, use for logging errors to console");
string errorMessage = "This is an error, use for logging errors to console";
context.error("Error: ", errorMessage);
return context.getRes().text("Check the Appwrite Console to see logs and errors!");
}
@@ -1121,9 +1287,11 @@ namespace runtime {
class Handler {
public:
static RuntimeOutput main(RuntimeContext &context) {
context.log("This is a log, use for logging information to console");
const std::string message = "This is a log, use for logging information to console";
context.log("Message: ", message);
context.log("This function was called with " + context.req.method + " method");
context.error("This is an error, use for logging errors to console");
const std::string errorMessage = "This is an error, use for logging errors to console";
context.error("Error: ", errorMessage);
return context.res.text("Check the Appwrite Console to see logs and errors!");
}
@@ -1142,14 +1310,18 @@ You can access these logs through the following steps.
# Accessing environment variables {% #environment-variables %}
If you need to pass constants or secrets to Appwrite Functions, you can use environment variables.
| Variable | Description |
|-----------------------------------|------------------------------------------------|
| `APPWRITE_FUNCTION_ID` | The ID of the running function. |
| `APPWRITE_FUNCTION_NAME` | The Name of the running function. |
| `APPWRITE_FUNCTION_DEPLOYMENT` | The deployment ID of the running function. |
| `APPWRITE_FUNCTION_PROJECT_ID` | The project ID of the running function. |
| `APPWRITE_FUNCTION_RUNTIME_NAME` | The runtime of the running function. |
| `APPWRITE_FUNCTION_RUNTIME_VERSION` | The runtime version of the running function. |
| Variable | Description | Available at Build and/or Run Time |
|-----------------------------------|------------------------------------------------|-----------------------------------------------------|
| `APPWRITE_FUNCTION_API_ENDPOINT` | The API endpoint of the running function | Both |
| `APPWRITE_VERSION` | The Appwrite version used to run the function | Both |
| `APPWRITE_REGION` | The region where the function will run from | Both |
| `APPWRITE_FUNCTION_API_KEY` | The function API key is used for server authentication | Build time |
| `APPWRITE_FUNCTION_ID` | The ID of the running function. | Both |
| `APPWRITE_FUNCTION_NAME` | The Name of the running function. | Both |
| `APPWRITE_FUNCTION_DEPLOYMENT` | The deployment ID of the running function. | Both |
| `APPWRITE_FUNCTION_PROJECT_ID` | The project ID of the running function. | Both |
| `APPWRITE_FUNCTION_RUNTIME_NAME` | The runtime of the running function. | Both |
| `APPWRITE_FUNCTION_RUNTIME_VERSION` | The runtime version of the running function. | Both |
{% arrow_link href="/docs/products/functions/functions#environment-variables" %}
Learn to add variables to you function
@@ -1158,7 +1330,7 @@ Learn to add variables to you function
You can access the environment variables through the systems library of each language.
{% multicode %}
```js
```server-nodejs
export default async ({ req, res, log }) => {
return res.text(process.env.MY_VAR);
}
@@ -1184,6 +1356,19 @@ export default async ({ req, res, log }) => {
return res.text(Deno.env.get('MY_VAR'));
}
```
```go
package handler
import (
"os"
"github.com/open-runtimes/types-for-go/v4/openruntimes"
)
func Main(Context openruntimes.Context) openruntimes.Response {
return res.text(os.Getenv(MY_VAR))
}
```
```dart
import 'dart:io';
import 'dart:async';
@@ -1298,6 +1483,12 @@ By default, we include the following package managers in each runtime.
* deno
* `deno cache <ENTRYPOINT_FILE>`
---
* {% only_dark %}{% icon_image src="/images/platforms/dark/go.svg" alt="Go logo" size="m" /%}{% /only_dark %}
{% only_light %}{% icon_image src="/images/platforms/go.svg" alt="Go logo" size="m" /%}{% /only_light %}
* Go
* Go Modules
* N/A
---
* {% only_dark %}{% icon_image src="/images/platforms/dark/dart.svg" alt="Dart logo" size="m" /%}{% /only_dark %}
{% only_light %}{% icon_image src="/images/platforms/dart.svg" alt="Dart logo" size="m" /%}{% /only_light %}
* Dart
@@ -1375,7 +1566,7 @@ export default async ({ req, res, log, error }) => {
try {
await databases.createDocument(
'<DATABASE_ID>',
'[COLLECTION_ID]',
'<COLLECTION_ID>',
ID.unique(),
{}
)
@@ -1408,7 +1599,7 @@ return function ($context) {
try {
$databases->createDocument(
databaseId: '<DATABASE_ID>',
collectionId: '[COLLECTION_ID]',
collectionId: '<COLLECTION_ID>',
documentId: ID::unique(),
data: []
);
@@ -1466,7 +1657,7 @@ def main(context)
begin
databases.create_document(
databaseId: '<DATABASE_ID>',
collectionId: '[COLLECTION_ID]',
collectionId: '<COLLECTION_ID>',
documentId: ID.unique(),
data: {}
)
@@ -1504,6 +1695,42 @@ export default function ({req, res, error}: any){
return res.text("Document created");
}
```
```go
package handler
import (
"fmt"
"os"
"github.com/appwrite/sdk-for-go/appwrite"
"github.com/appwrite/sdk-for-go/id"
"github.com/open-runtimes/types-for-go/v4/openruntimes"
)
func Main(Context openruntimes.Context) openruntimes.Response {
// Set project and set API key
client := appwrite.NewClient(
appwrite.WithProject(os.Getenv("APPWRITE_FUNCTION_PROJECT_ID")),
appwrite.WithKey(Context.Req.Headers["x-appwrite-key"]),
)
databases := appwrite.NewDatabases(client)
_, err := databases.CreateDocument(
"<DATABASE_ID>",
"<COLLECTION_ID>",
id.Unique(),
map[string]interface{}{},
)
if err != nil {
Context.Log(fmt.Sprintf("Failed to create document: %v", err))
return Context.Res.Text("Failed to create document")
}
return Context.Res.Text("Document created")
}
```
```dart
import 'dart:io';
import 'dart:async';
@@ -1520,7 +1747,7 @@ Future<dynamic> main(final context) async {
try {
await databases.createDocument(
databaseId: '<DATABASE_ID>',
collectionId: '[COLLECTION_ID]',
collectionId: '<COLLECTION_ID>',
documentId: ID.unique(),
data: {}
);
@@ -1683,7 +1910,7 @@ export default async ({ req, res, log }) => {
if (req.headers['x-appwrite-user-jwt']) {
client.setJWT(req.headers['x-appwrite-user-jwt'])
} else {
return res.text("Please sign in, JWT not found")
return res.text("Access denied: This function requires authentication. Please sign in to continue.");
}
const databases = new Databases(client);
@@ -1691,7 +1918,7 @@ export default async ({ req, res, log }) => {
try {
await databases.createDocument(
'<DATABASE_ID>',
'[COLLECTION_ID]',
'<COLLECTION_ID>',
ID.unique(),
{}
)
@@ -1720,7 +1947,7 @@ return function ($context) {
if (isset($context->req->headers['x-appwrite-user-jwt'])) {
$client->setJWT($context->req->headers['x-appwrite-user-jwt']);
} else {
return $context->res->text("Please sign in, JWT not found");
return $context->res->text("Access denied: This function requires authentication. Please sign in to continue.");
}
$databases = new Databases($client);
@@ -1728,7 +1955,7 @@ return function ($context) {
try {
$databases->createDocument(
databaseId: '<DATABASE_ID>',
collectionId: '[COLLECTION_ID]',
collectionId: '<COLLECTION_ID>',
documentId: ID::unique(),
data: []
);
@@ -1756,7 +1983,7 @@ def main(context):
if "x-appwrite-user-jwt" in context.req.headers:
client.set_jwt(context.req.headers["x-appwrite-user-jwt"])
else:
return context.res.text("Please sign in, JWT not found")
return context.res.text("Access denied: This function requires authentication. Please sign in to continue.")
databases = Databases(client)
@@ -1785,13 +2012,13 @@ def main(context)
if context.request.headers['x-appwrite-user-jwt']
client.set_jwt(context.request.headers['x-appwrite-user-jwt'])
else
return context.response.text("Please sign in, JWT not found")
return context.response.text("Access denied: This function requires authentication. Please sign in to continue.")
end
databases = Appwrite::Databases.new(client)
begin
databases.create_document('<DATABASE_ID>', '[COLLECTION_ID]', Appwrite::ID.unique(), {})
databases.create_document('<DATABASE_ID>', '<COLLECTION_ID>', Appwrite::ID.unique(), {})
rescue Appwrite::Exception => e
context.error("Failed to create document: " + e.message)
return context.response.text("Failed to create document")
@@ -1810,7 +2037,7 @@ export default function ({req, res, error}: any){
if (req.headers["x-appwrite-user-jwt"]) {
client.setJWT(req.headers["x-appwrite-user-jwt"]);
} else {
return res.text("Please sign in, JWT not found");
return res.text("Access denied: This function requires authentication. Please sign in to continue.");
}
const databases = new Databases(client);
@@ -1830,6 +2057,47 @@ export default function ({req, res, error}: any){
return res.text("Document created");
}
```
```go
package handler
import (
"fmt"
"log"
"github.com/appwrite/sdk-for-go/appwrite"
"github.com/appwrite/sdk-for-go/id"
"github.com/open-runtimes/types-for-go/v4/openruntimes"
)
func Main(Context openruntimes.Context) openruntimes.Response {
client := appwrite.NewClient(
appwrite.WithProject("APPWRITE_FUNCTION_PROJECT_ID"),
)
jwt, exists := Context.Req.Headers["x-appwrite-user-jwt"]
if !exists || len(jwt) == 0 {
appwrite.WithJWT(Context.Req.Headers["x-appwrite-user-jwt"])
} else {
return Context.Res.Text("Access denied: This function requires authentication. Please sign in to continue.")
}
databases := appwrite.NewDatabases(client)
_, err := databases.CreateDocument(
"<DATABASE_ID>",
"<COLLECTION_ID>",
id.Unique(),
map[string]interface{}{},
)
if err != nil {
Context.Log(fmt.Sprintf("Failed to create document: %v", err))
return Context.Res.Text(str)
}
return Context.Res.Text("Document created")
}
```
```dart
import 'dart:io';
import 'dart:async';
@@ -1842,7 +2110,7 @@ Future<dynamic> main(final context) async {
if (context.req.headers['x-appwrite-user-jwt'] != null) {
client.setJWT(context.req.headers['x-appwrite-user-jwt']);
} else {
return context.res.text("Please sign in, JWT not found");
return context.res.text("Access denied: This function requires authentication. Please sign in to continue.");
}
final databases = Databases(client);
@@ -1850,7 +2118,7 @@ Future<dynamic> main(final context) async {
try {
await databases.createDocument(
databaseId: '<DATABASE_ID>',
collectionId: '[COLLECTION_ID]',
collectionId: '<COLLECTION_ID>',
documentId: ID.unique(),
data: {}
);
@@ -1874,7 +2142,7 @@ func main(context: RuntimeContext) async throws -> RuntimeOutput {
if let jwt = context.req.headers["x-appwrite-user-jwt"] {
client.setJWT(jwt)
} else {
return context.res.text("Please sign in, JWT not found")
return context.res.text("Access denied: This function requires authentication. Please sign in to continue.")
}
let databases = Databases(client: client)
@@ -1911,7 +2179,7 @@ namespace DotNetRuntime
if (Context.Req.Headers.ContainsKey("x-appwrite-user-jwt")) {
client.SetJWT(Context.Req.Headers["x-appwrite-user-jwt"]);
} else {
return Context.Res.Text("Please sign in, JWT not found");
return Context.Res.Text("Access denied: This function requires authentication. Please sign in to continue");
}
var databases = new Databases(client);
@@ -1950,7 +2218,7 @@ class Main {
if (context.req.headers["x-appwrite-user-jwt"] != null) {
client.setJWT(context.req.headers["x-appwrite-user-jwt"])
} else {
return context.res.text("Please sign in, JWT not found")
return context.res.text("Access denied: This function requires authentication. Please sign in to continue.")
}
val databases = Databases(client)
@@ -1987,7 +2255,7 @@ public class Main {
if (context.req.headers.containsKey("x-appwrite-user-jwt")) {
client.setJWT(context.req.headers.get("x-appwrite-user-jwt"));
} else {
return context.res.text("Please sign in, JWT not found");
return context.res.text("Access denied: This function requires authentication. Please sign in to continue.");
}
Databases databases = new Databases(client);
@@ -2017,13 +2285,13 @@ As your functions grow, you may find yourself needing to split your code into mu
{% tabs %}
{% tabsitem #nodejs title="Node.js" %}
```js
```server-nodejs
// src/utils.js
export function add(a, b) {
return a + b;
}
```
```js
```server-nodejs
// src/main.js
import { add } from './utils.js';
@@ -2101,6 +2369,45 @@ export default function ({res}: {res: any}) {
```
{% /tabsitem %}
{% tabsitem #go title="Go" %}
```go
// src/utils/go.mod
module example.com/utils
go 1.23.0
```
```go
// src/utils/utils.go
package utils
func Add(a int, b int) int {
return a + b
}
```
```go
// src/main/go.mod
module example.com/main
go 1.23.0
replace example.com/utils => ../utils // Run go mod edit -replace example.com/go=../go
require example.com/utils v0.0.0-00010101000000-000000000000 // Run go mod tidy
```
```go
// src/main/main.go
package main
import "example.com/utils"
func main() {
// Get a greeting message and print it.
message := utils.Add(5, 4)
print(message)
}
```
{% /tabsitem %}
{% tabsitem #dart title="Dart" %}
```dart
// lib/utils.dart

View File

@@ -157,14 +157,16 @@ import { Client, Account } from "appwrite";
// Generate a unique ID
ID.unique()
// Generate a custom ID
ID.custom()
ID.custom("my-custom-id")
```
```client-flutter
import 'package:appwrite/appwrite.dart';
// Generate a unique ID
ID.unique()
// Generate a custom ID
ID.custom()
```
@@ -173,16 +175,18 @@ import Appwrite
// Generate a unique ID
ID.unique()
// Generate a custom ID
ID.custom()
ID.custom("my-custom-id")
```
```client-android-kotlin
import io.appwrite.ID
// Generate a unique ID
ID.unique()
// Generate a custom ID
ID.custom()
ID.custom("my-custom-id")
```
{% /multicode %}
{% /tabsitem %}
@@ -194,16 +198,18 @@ const sdk = require('node-appwrite');
// Generate a unique ID
sdk.ID.unique()
// Generate a custom ID
sdk.ID.custom()
sdk.ID.custom("my-custom-id")
```
```deno
import * as sdk from "https://deno.land/x/appwrite/mod.ts";
// Generate a unique ID
sdk.ID.unique()
// Generate a custom ID
sdk.ID.custom()
sdk.ID.custom("my-custom-id")
```
```php
<?php
@@ -212,63 +218,71 @@ use Appwrite\ID;
// Generate a unique ID
ID::unique()
// Generate a custom ID
ID::custom()
ID::custom("my-custom-id")
```
```python
from appwrite.id import ID
# Generate a unique ID
ID.unique()
# Generate a custom ID
ID.custom()
ID.custom("my-custom-id")
```
```csharp
using Appwrite.ID;
// Generate a unique ID
ID.unique()
// Generate a custom ID
ID.custom()
ID.custom("my-custom-id")
```
```dart
import 'package:dart_appwrite/dart_appwrite.dart';
// Generate a unique ID
ID.unique()
// Generate a custom ID
ID.custom()
ID.custom("my-custom-id")
```
```java
io.appwrite.ID
// Generate a unique ID
ID.unique()
// Generate a custom ID
ID.custom()
ID.custom("my-custom-id")
```
```kotlin
io.appwrite.ID
// Generate a unique ID
ID.unique()
// Generate a custom ID
ID.custom()
ID.custom("my-custom-id")
```
```ruby
require 'appwrite'
include Appwrite
// Generate a unique ID
ß
# Generate a unique ID
ID.unique()
// Generate a custom ID
ID.custom()
# Generate a custom ID
ID.custom("my-custom-id")
```
```swift
import Appwrite
// Generate a unique ID
ID.unique()
// Generate a custom ID
ID.custom()
ID.custom("my-custom-id")
```
{% /multicode %}
{% /tabsitem %}

View File

@@ -4,7 +4,7 @@
const parent: NavParent = {
href: '/docs',
label: 'Command Line'
label: 'CLI'
};
const navigation: NavTree = [

View File

@@ -1,358 +1,357 @@
<script lang="ts">
import { browser } from "$app/environment";
import { Tabs } from "$lib/UI";
import { visible } from "$lib/actions/visible";
import { isHeaderHidden } from "$lib/layouts/Main.svelte";
import { getScrollDir } from "$lib/utils/getScrollDir";
import { isVisible } from "$lib/utils/isVisible";
import { createAccordion, melt } from "@melt-ui/svelte";
import { writable } from "svelte/store";
import { fly } from "svelte/transition";
import { browser } from '$app/environment';
import { Tabs } from '$lib/UI';
import { visible } from '$lib/actions/visible';
import { isHeaderHidden } from '$lib/layouts/Main.svelte';
import { getScrollDir } from '$lib/utils/getScrollDir';
import { createAccordion, melt } from '@melt-ui/svelte';
import { writable } from 'svelte/store';
import { fly } from 'svelte/transition';
import Tooltip from '../../lib/components/Tooltip.svelte';
type Table = {
title: string;
rows: {
title: string;
info?: string;
free: string | true;
pro: string | true;
scale: string | true;
}[];
};
const cols = ["free", "pro", "scale"] as const;
const cols = ['free', 'pro', 'scale'] as const;
const tables = [
{
title: "Resources",
title: 'Resources',
rows: [
{
title: "Bandwidth",
free: "10GB",
pro: "300GB",
scale: "300GB",
title: 'Bandwidth',
free: '10GB',
pro: '300GB',
scale: '300GB'
},
{
title: "Additional bandwidth",
free: "-",
pro: "$40 per 100GB",
scale: "$40 per 100GB",
title: 'Additional bandwidth',
free: '-',
pro: '$40 per 100GB',
scale: '$40 per 100GB'
},
{
title: "Storage",
free: "2GB",
pro: "150GB",
scale: "150GB",
title: 'Storage',
free: '2GB',
pro: '150GB',
scale: '150GB'
},
{
title: "Additional storage",
free: "-",
pro: "$3 per 100GB",
scale: "$3 per 100GB",
title: 'Additional storage',
free: '-',
pro: '$3 per 100GB',
scale: '$3 per 100GB'
},
{
title: "Compute",
free: "750K executions",
pro: "3.5M executions",
scale: "3.5M executions",
},
],
title: 'Compute',
free: '750K executions',
pro: '3.5M executions',
scale: '3.5M executions'
}
]
},
{
title: "Platform",
title: 'Platform',
rows: [
{
title: "Number of projects",
free: "Unlimited",
pro: "Unlimited",
scale: "Unlimited",
title: 'Number of projects',
free: 'Unlimited',
pro: 'Unlimited',
scale: 'Unlimited'
},
{
title: "Projects pausing",
free: "Never",
pro: "Never",
scale: "Never",
title: 'Projects pausing',
free: 'Never',
pro: 'Never',
scale: 'Never'
},
{
title: "Organization Members",
free: "1",
pro: "1",
scale: "Unlimited",
title: 'Organization Members',
free: '1',
pro: '1',
scale: 'Unlimited'
},
{
title: "Additional Organization members",
free: "-",
pro: "$15 per member",
scale: "$0",
title: 'Additional Organization members',
free: '-',
pro: '$15 per member',
scale: '$0'
},
{
title: "Connected websites and apps",
free: "3 per project",
pro: "Unlimited",
scale: "Unlimited",
title: 'Connected websites and apps',
free: '3 per project',
pro: 'Unlimited',
scale: 'Unlimited'
},
{
title: "Custom domains",
free: "Unlimited",
pro: "Unlimited",
scale: "Unlimited",
title: 'Custom domains',
free: 'Unlimited',
pro: 'Unlimited',
scale: 'Unlimited'
},
{
title: "No Appwrite branding on emails",
free: "-",
title: 'No Appwrite branding on emails',
free: '-',
pro: true,
scale: true,
scale: true
},
{
title: "Custom SMTP",
free: "-",
title: 'Custom SMTP',
free: '-',
pro: true,
scale: true,
scale: true
},
{
title: "Webhooks",
free: "2 per project",
pro: "Unlimited",
scale: "Unlimited",
title: 'Webhooks',
free: '2 per project',
pro: 'Unlimited',
scale: 'Unlimited'
},
{
title: "Logs retention",
free: "1 hour",
pro: "7 days",
scale: "28 days",
title: 'Logs retention',
free: '1 hour',
pro: '7 days',
scale: '28 days'
},
{
title: "Budget caps and alerts",
free: "Not needed",
title: 'Budget caps and alerts',
free: 'Not needed',
pro: true,
scale: true,
},
],
scale: true
}
]
},
{
title: "Auth",
title: 'Auth',
rows: [
{
title: "Users",
free: "75,000 monthly active users",
pro: "200,000 monthly active users",
scale: "200,000 monthly active users",
title: 'Users',
free: '75,000 monthly active users',
pro: '200,000 monthly active users',
scale: '200,000 monthly active users'
},
{
title: "Additional users",
free: "-",
pro: "$3 per 1,000 users",
scale: "$3 per 1,000 users",
title: 'Additional users',
free: '-',
pro: '$3 per 1,000 users',
scale: '$3 per 1,000 users'
},
{
title: "Teams",
free: "100 per project",
pro: "Unlimited",
scale: "Unlimited",
title: 'Teams',
free: '100 per project',
pro: 'Unlimited',
scale: 'Unlimited'
},
{
title: "SSO",
free: "-",
pro: "-",
scale: "Coming soon",
},
],
title: 'SSO',
free: '-',
pro: '-',
scale: 'Coming soon'
}
]
},
{
title: "Databases",
title: 'Databases',
rows: [
{
title: "Databases",
free: "1 per project",
pro: "Unlimited",
scale: "Unlimited",
title: 'Databases',
free: '1 per project',
pro: 'Unlimited',
scale: 'Unlimited'
},
{
title: "Documents",
free: "Unlimited",
pro: "Unlimited",
scale: "Unlimited",
title: 'Documents',
free: 'Unlimited',
pro: 'Unlimited',
scale: 'Unlimited'
},
{
title: "Reads & Writes",
free: "Unlimited",
pro: "Unlimited",
scale: "Unlimited",
title: 'Reads & Writes',
free: 'Unlimited',
pro: 'Unlimited',
scale: 'Unlimited'
},
{
title: "Dedicated databases",
free: "-",
pro: "Coming Soon",
scale: "Coming soon",
},
],
title: 'Dedicated databases',
free: '-',
pro: 'Coming Soon',
scale: 'Coming soon'
}
]
},
{
title: "Storage",
title: 'Storage',
rows: [
{
title: "Buckets",
free: "3 per project",
pro: "Unlimited",
scale: "Unlimited",
title: 'Buckets',
free: '3 per project',
pro: 'Unlimited',
scale: 'Unlimited'
},
{
title: "File size limit",
free: "50MB",
pro: "5GB",
scale: "5GB",
title: 'File size limit',
free: '50MB',
pro: '5GB',
scale: '5GB'
},
{
title: "Image transformations",
free: "Unlimited",
pro: "Unlimited",
scale: "Unlimited",
},
],
title: 'Image transformations',
free: 'Unlimited',
pro: 'Unlimited',
scale: 'Unlimited'
}
]
},
{
title: "Functions",
title: 'Functions',
rows: [
{
title: "Functions",
free: "5 per project",
pro: "Unlimited",
scale: "Unlimited",
title: 'Functions',
free: '5 per project',
pro: 'Unlimited',
scale: 'Unlimited'
},
{
title: "Executions",
free: "750K",
pro: "3.5M",
scale: "3.5M",
title: 'Executions',
free: '750K',
pro: '3.5M',
scale: '3.5M'
},
{
title: "Additional executions",
free: "-",
pro: "$2 per 1 Million",
scale: "$2 per 1 Million",
title: 'Additional executions',
free: '-',
pro: '$2 per 1 Million',
scale: '$2 per 1 Million'
},
{
title: "Express builds",
free: "-",
title: 'Express builds',
info: 'Dedicated priority queues for build jobs',
free: '-',
pro: true,
scale: true,
},
],
scale: true
}
]
},
{
title: "Realtime",
title: 'Realtime',
rows: [
{
title: "Concurrent connections",
free: "250",
pro: "500",
scale: "500",
title: 'Concurrent connections',
free: '250',
pro: '500',
scale: '500'
},
{
title: "Additional concurrent connections",
free: "-",
pro: "$5 per 1,000",
scale: "$5 per 1,000",
title: 'Additional concurrent connections',
free: '-',
pro: '$5 per 1,000',
scale: '$5 per 1,000'
},
{
title: "Messages",
free: "3M",
pro: "Unlimited",
scale: "Unlimited",
},
],
title: 'Messages',
free: '3M',
pro: 'Unlimited',
scale: 'Unlimited'
}
]
},
{
title: "Security",
title: 'Security',
rows: [
{
title: "SOC-2",
free: "-",
pro: "-",
scale: true,
title: 'SOC-2',
free: '-',
pro: '-',
scale: true
},
{
title: "HIPAA",
free: "-",
pro: "-",
scale: true,
title: 'HIPAA',
free: '-',
pro: '-',
scale: true
},
{
title: "BAA",
free: "-",
pro: "-",
scale: true,
title: 'BAA',
free: '-',
pro: '-',
scale: true
},
{
title: "Additional organization roles",
free: "-",
pro: "-",
scale: "Coming Soon",
title: 'Additional organization roles',
free: '-',
pro: '-',
scale: 'Coming Soon'
},
{
title: "Network logs",
free: "-",
pro: "-",
scale: "Coming Soon",
title: 'Network logs',
free: '-',
pro: '-',
scale: 'Coming Soon'
},
{
title: "Activity logs",
free: "-",
pro: "-",
scale: "Coming Soon",
},
],
title: 'Activity logs',
free: '-',
pro: '-',
scale: 'Coming Soon'
}
]
},
{
title: "Support",
title: 'Support',
rows: [
{
title: "Community",
title: 'Community',
free: true,
pro: true,
scale: true,
scale: true
},
{
title: "Email",
free: "-",
title: 'Email',
free: '-',
pro: true,
scale: true,
scale: true
},
{
title: "Priority",
free: "-",
pro: "-",
scale: true,
title: 'Priority',
free: '-',
pro: '-',
scale: true
},
{
title: "SLA",
free: "-",
pro: "-",
scale: true,
},
],
},
title: 'SLA',
free: '-',
pro: '-',
scale: true
}
]
}
] satisfies Table[];
let tab = "free";
let tab = 'free';
const {
elements: { root, trigger, content, heading, item },
states: { value },
states: { value }
} = createAccordion({
defaultValue: ["Resources"],
defaultValue: ['Resources'],
multiple: true,
forceVisible: true,
forceVisible: true
});
const visibleTables = writable([] as string[]);
$: activeTable = $visibleTables.sort((a, b) => {
return (
tables.findIndex((t) => t.title === a) -
tables.findIndex((t) => t.title === b)
);
return tables.findIndex((t) => t.title === a) - tables.findIndex((t) => t.title === b);
})[0];
let scrollDir = "down";
let scrollDir = 'down';
let shouldShowTable = false;
</script>
@@ -360,9 +359,9 @@
<div class="web-big-padding-section-level-1 web-white-section theme-light">
<div class="web-big-padding-section-level-2">
<div class="relative">
<div class="u-position-relative">
<article use:melt={$root}>
<div class="container">
<div class="web-container">
<header
class="web-u-text-align-center"
use:visible
@@ -371,14 +370,13 @@
}}
>
<h3 class="web-title web-u-color-text-primary">Compare plans</h3>
<p class="web-main-body-500 mt-4">
Discover our plans and find the one that fits your projects
needs.
<p class="web-main-body-500 u-margin-block-start-16">
Discover our plans and find the one that fits your projects needs.
</p>
</header>
<div
class="web-is-only-mobile web-u-padding-block-start-48 web-u-padding-inline-8 web-u-margin-inline-8-negative web-u-filter-blur-8"
class="web-is-only-mobile web-u-padding-block-start-48 web-u-padding-inline-8 web-u-margin-inline-8-negative web-u-filter-blur-8 /u-position-sticky /u-z-index-5"
style:--inset-block-start="2rem"
>
<Tabs bind:tab tabs={cols} let:TabsList>
@@ -390,34 +388,31 @@
--p-secondary-tabs-bg-color-selected: var(--web-color-accent) / 0.08;"
let:tab
>
<span class="web-main-body-500 capitalize">{tab}</span>
<span class="web-main-body-500 u-capitalize">{tab}</span>
</TabsList>
</Tabs>
</div>
<div
class="web-is-not-mobile web-u-grid-auto-column-1fr is-with-footer-border web-u-padding-inline-8 web-u-margin-inline-8-negative web-u-filter-blur-8 web-u-container-query-inline sticky z-[5] gap-8"
style:--inset-block-start={$isHeaderHidden ? "0px" : "70px"}
class="web-is-not-mobile web-u-grid-auto-column-1fr is-with-footer-border u-gap-32 web-u-padding-inline-8 web-u-margin-inline-8-negative web-u-filter-blur-8 u-position-sticky u-z-index-5 web-u-container-query-inline"
style:--inset-block-start={$isHeaderHidden ? '0px' : '70px'}
style:transition="inset-block-start 0.3s ease"
>
<div
class="web-description web-u-color-text-primary web-self-center"
class="web-description web-u-color-text-primary web-u-cross-child-center"
style:opacity={browser ? 1 : 0}
style:position={browser ? "relative" : undefined}
style:position={browser ? 'relative' : undefined}
>
<span style="opacity: 0;">{activeTable}</span>
{#key `${activeTable}-${shouldShowTable}`}
<div
style="position: absolute; top: 50%; transform: translateY(-50%);"
in:fly={{
y: scrollDir === "down" ? 16 : -16,
y: scrollDir === 'down' ? 16 : -16,
delay: 250,
duration: 250,
}}
out:fly={{
y: scrollDir === "down" ? -16 : 16,
duration: 250,
duration: 250
}}
out:fly={{ y: scrollDir === 'down' ? -16 : 16, duration: 250 }}
>
{#if shouldShowTable && activeTable}
{activeTable}
@@ -426,7 +421,9 @@
{/key}
</div>
<div class="web-mini-card">
<div class="flex flex-wrap items-center justify-between gap-4">
<div
class="u-flex u-cross-center u-gap-16 u-flex-wrap u-main-space-between"
>
<h4 class="web-label web-u-color-text-primary">Free</h4>
<a
href="https://cloud.appwrite.io/register"
@@ -437,7 +434,9 @@
</div>
</div>
<div class="web-mini-card">
<div class="flex flex-wrap items-center justify-between gap-4">
<div
class="u-flex u-cross-center u-gap-16 u-flex-wrap u-main-space-between"
>
<h4 class="web-label web-u-color-text-primary">Pro</h4>
<a
class="web-button"
@@ -451,7 +450,9 @@
</div>
</div>
<div class="web-mini-card">
<div class="flex flex-wrap items-center justify-between gap-4">
<div
class="u-flex u-cross-center u-gap-16 u-flex-wrap u-main-space-between"
>
<h4 class="web-label web-u-color-text-primary">Scale</h4>
<button class="web-button is-secondary" disabled>
<span class="web-sub-body-500">Coming soon</span>
@@ -482,7 +483,7 @@
<caption
class="web-compare-table-caption web-description web-u-color-text-primary"
use:melt={$heading({ level: 3 })}
style:position={browser ? "unset" : undefined}
style:position={browser ? 'unset' : undefined}
>
<button
class="web-compare-table-caption-button"
@@ -496,16 +497,28 @@
</button>
</caption>
<tbody
class="web-compare-table-body"
use:melt={$content(table.title)}
>
<tbody class="web-compare-table-body" use:melt={$content(table.title)}>
{#each table.rows as row}
<tr>
<th class="web-sub-body-500">{row.title}</th>
<th class="web-sub-body-500">
<div class="u-flex u-gap-4">
{row.title}
{#if row.info}
<Tooltip placement="top">
<span class="icon-info" aria-hidden="true" />
<svelte:fragment slot="tooltip">
{row.info}
</svelte:fragment>
</Tooltip>
{/if}
</div>
</th>
{#each cols as col, index}
<td class="level-{index}" class:is-selected={col === tab}>
{#if typeof row[col] === "string"}
<td
class="level-{index}"
class:is-selected={col === tab}
>
{#if typeof row[col] === 'string'}
{row[col]}
{:else}
<img

View File

@@ -19,6 +19,7 @@
body: JSON.stringify({
email,
key,
cloud: true /* not optional on the growth endpoint. */,
}),
},
);

2
terraform/.env.example Normal file
View File

@@ -0,0 +1,2 @@
export TF_VAR_DO_TOKEN=
export TF_VAR_PRIVATE_KEY=

View File

@@ -0,0 +1,16 @@
module "droplets" {
source = "../../modules/digitalocean"
private_key = "${var.PRIVATE_KEY}"
project_name = "hmp"
region = "fra1"
environment = "prd"
base_image = "docker-20-04"
worker_size = "s-2vcpu-2gb-amd"
worker_count = 6
subnet_range = "10.117.0.0/20"
manager_size = "s-2vcpu-2gb-amd"
manager_count = 2
digitalocean_project_name = "Production - Homepage"
}

View File

@@ -0,0 +1,18 @@
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
cloud {
organization = "appwrite"
workspaces {
name = "production-homepage"
}
}
}
provider "digitalocean" {
token = var.DO_TOKEN
}

View File

@@ -0,0 +1,7 @@
variable "DO_TOKEN" {
description = "DigitalOcean API token"
}
variable "PRIVATE_KEY" {
description = "Contents of your local SSH private key file"
default = "$(cat ~/.ssh/id_rsa)"
}

View File

@@ -0,0 +1,16 @@
module "droplets" {
source = "../../modules/digitalocean"
private_key = "${var.PRIVATE_KEY}"
project_name = "hmp"
region = "fra1"
environment = "stg"
base_image = "docker-20-04"
subnet_range = "10.116.0.0/20"
worker_size = "s-1vcpu-2gb"
worker_count = 4
manager_size = "s-1vcpu-2gb"
manager_count = 2
digitalocean_project_name = "Staging - Homepage"
}

View File

@@ -0,0 +1,18 @@
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
cloud {
organization = "appwrite"
workspaces {
name = "staging-homepage"
}
}
}
provider "digitalocean" {
token = var.DO_TOKEN
}

View File

@@ -0,0 +1,7 @@
variable "DO_TOKEN" {
description = "DigitalOcean API token"
}
variable "PRIVATE_KEY" {
description = "Contents of your local SSH private key file"
default = "$(cat ~/.ssh/id_rsa)"
}

View File

@@ -0,0 +1,153 @@
locals {
mount_nfs = "/letsencrypt"
setup_firewall = [
"ufw allow 2377/tcp",
"ufw allow 7946/tcp",
"ufw allow 7946/udp",
"ufw allow 4789/udp",
"ufw reload",
"systemctl restart docker"
]
setup_nfs = [
"ufw allow 2049",
"ufw reload",
"apt install -y nfs-common",
"mkdir -p ${local.mount_nfs}",
"echo '${digitalocean_droplet.nfs.ipv4_address_private}:${local.mount_nfs} ${local.mount_nfs} nfs proto=tcp,port=2049,nfsvers=4,sync,noexec,rw 0 0' >> /etc/fstab",
"mount -a",
]
}
resource "digitalocean_project" "homepage" {
name = var.digitalocean_project_name
description = "Appwrite Homepage"
purpose = "Web Application"
environment = "Development"
resources = flatten([
digitalocean_droplet.leader.urn,
digitalocean_droplet.manager[*].urn,
digitalocean_droplet.worker[*].urn,
digitalocean_droplet.nfs.urn
])
}
# Tags
resource "digitalocean_tag" "worker" {
name = "${var.environment}-worker"
}
resource "digitalocean_tag" "manager" {
name = "${var.environment}-manager"
}
resource "digitalocean_droplet" "leader" {
image = var.base_image
name = "${var.project_name}-${var.region}-${var.environment}-leader-0"
region = var.region
size = var.manager_size
tags = [digitalocean_tag.manager.id]
ssh_keys = [
data.digitalocean_ssh_key.Christy.id
]
vpc_uuid = digitalocean_vpc.subnet.id
connection {
host = self.ipv4_address
user = "root"
type = "ssh"
private_key = var.private_key
timeout = "2m"
}
provisioner "remote-exec" {
inline = concat(local.setup_firewall, local.setup_nfs, [
"docker swarm init --advertise-addr ${self.ipv4_address_private}"
])
}
}
resource "digitalocean_droplet" "manager" {
count = var.manager_count
image = var.base_image
name = "${var.project_name}-${var.region}-${var.environment}-manager-${count.index}"
region = var.region
size = var.manager_size
tags = [digitalocean_tag.manager.id]
vpc_uuid = digitalocean_vpc.subnet.id
ssh_keys = [
data.digitalocean_ssh_key.Christy.id
]
connection {
host = self.ipv4_address
user = "root"
type = "ssh"
private_key = var.private_key
timeout = "2m"
}
provisioner "remote-exec" {
inline = concat(local.setup_firewall, local.setup_nfs, [
"docker swarm join --token ${data.external.swarm_join_token.result.manager} ${digitalocean_droplet.leader.ipv4_address_private}:2377"
])
}
}
resource "digitalocean_droplet" "worker" {
count = var.worker_count
image = var.base_image
name = "${var.project_name}-${var.region}-${var.environment}-worker-${count.index}"
region = var.region
size = var.worker_size
tags = [digitalocean_tag.worker.id]
vpc_uuid = digitalocean_vpc.subnet.id
ssh_keys = [
data.digitalocean_ssh_key.Christy.id
]
connection {
host = self.ipv4_address
user = "root"
type = "ssh"
private_key = var.private_key
timeout = "2m"
}
provisioner "remote-exec" {
inline = concat(local.setup_firewall, [
"docker swarm join --token ${data.external.swarm_join_token.result.worker} ${digitalocean_droplet.leader.ipv4_address_private}:2377"
])
}
}
resource "digitalocean_droplet" "nfs" {
image = var.base_image
name = "${var.project_name}-${var.region}-${var.environment}-nfs-0"
region = var.region
size = var.worker_size
vpc_uuid = digitalocean_vpc.subnet.id
ssh_keys = [
data.digitalocean_ssh_key.Christy.id
]
connection {
host = self.ipv4_address
user = "root"
type = "ssh"
private_key = var.private_key
timeout = "2m"
}
provisioner "remote-exec" {
inline = [
"ufw allow 2049",
"ufw reload",
"sudo apt update",
"sudo apt install -y nfs-kernel-server",
"mkdir -p ${local.mount_nfs}",
"echo '${local.mount_nfs} ${var.subnet_range}(rw,sync,no_root_squash,no_subtree_check)' >> /etc/exports",
"exportfs -arvf",
"systemctl restart nfs-kernel-server",
]
}
}

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Exit if any of the intermediate steps fail
set -e
# Extract input variables
eval "$(jq -r '@sh "HOST=\(.host)"')"
# Get worker join token
WORKER=$(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$HOST docker swarm join-token worker -q)
MANAGER=$(ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@$HOST docker swarm join-token manager -q)
# Pass back a JSON object
jq -n --arg worker $WORKER --arg manager $MANAGER '{"worker":$worker,"manager":$manager}'

View File

@@ -0,0 +1,75 @@
# VPC
resource "digitalocean_vpc" "subnet" {
name = "${var.environment}-subnet"
region = var.region
ip_range = var.subnet_range
}
# Firewall Rules
resource "digitalocean_firewall" "web" {
name = "${var.environment}-web"
tags = [digitalocean_tag.worker.id, digitalocean_tag.manager.id]
# HTTP/HTTPS
inbound_rule {
protocol = "tcp"
port_range = "80"
source_addresses = ["0.0.0.0/0", "::/0"]
}
inbound_rule {
protocol = "tcp"
port_range = "443"
source_addresses = ["0.0.0.0/0", "::/0"]
}
# Outbound communication
outbound_rule {
protocol = "tcp"
port_range = "all"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
outbound_rule {
protocol = "udp"
port_range = "all"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
outbound_rule {
protocol = "icmp"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
}
resource "digitalocean_firewall" "vpc_communication" {
name = "${var.environment}-vpc-communication"
droplet_ids = [ digitalocean_droplet.nfs.id ]
tags = [digitalocean_tag.worker.id, digitalocean_tag.manager.id]
# Internal communication
inbound_rule {
protocol = "tcp"
port_range = "all"
source_addresses = [var.subnet_range]
}
inbound_rule {
protocol = "udp"
port_range = "all"
source_addresses = [var.subnet_range]
}
}
resource "digitalocean_firewall" "ssh" {
name = "${var.environment}-ssh"
droplet_ids = [ digitalocean_droplet.nfs.id ]
tags = [digitalocean_tag.worker.id, digitalocean_tag.manager.id]
# SSH
inbound_rule {
protocol = "tcp"
port_range = "22"
source_addresses = ["0.0.0.0/0", "::/0"]
}
}

View File

@@ -0,0 +1,4 @@
output "leader_public_ip" {
value = digitalocean_droplet.leader.ipv4_address
description = "The public IP address of the leader node"
}

View File

@@ -0,0 +1,8 @@
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
}

View File

@@ -0,0 +1,44 @@
variable "private_key" {
description = "The path to the private key used to SSH into the droplets"
}
variable "project_name" {
description = "Name for the current infrastructure project"
}
variable "region" {
description = "The region to deploy the infrastructure to. See https://docs.digitalocean.com/products/platform/availability-matrix/#available-datacenters"
}
variable "environment" {
description = "Name of the current environment"
}
variable "base_image" {
description = "Base Image to use for all droplets"
}
variable "subnet_range" {
description = "Subnet range for the VPC"
}
variable "worker_size" {
description = "Size of the NFS node. See https://slugs.do-api.dev/"
}
variable "worker_count" {
description = "Count of worker nodes required"
}
variable "manager_size" {
description = "Size of the manager node. See https://slugs.do-api.dev/"
}
variable "manager_count" {
description = "Count of API nodes required"
}
variable "digitalocean_project_name" {
description = "Name of the DigitalOcean Project"
}
data "digitalocean_ssh_key" "Christy" {
name = "Christy"
}
data "external" "swarm_join_token" {
program = ["${path.module}/get-join-token.sh"]
query = {
host = "${digitalocean_droplet.leader.ipv4_address}"
}
}