diff --git a/README.md b/README.md
index fa2bebf..ec8f3e8 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,7 @@ The current `golemcore/*` modules in this repository are:
| `golemcore/tavily-search` | Tavily-backed web search tool plugin with configurable search depth and answer generation. |
| `golemcore/telegram` | Telegram channel, invite onboarding, confirmations, and plan approval integration. |
| `golemcore/slack` | Slack Socket Mode channel plugin with thread follow-ups, confirmations, and plan approval UI. |
+| `golemcore/supabase` | Supabase PostgREST rows plugin with configurable read/write operations for database tables. |
| `golemcore/weather` | Open-Meteo weather tool plugin with no external credentials required. |
| `golemcore/whisper` | Whisper-compatible speech-to-text provider plugin. |
diff --git a/golemcore/supabase/plugin.yaml b/golemcore/supabase/plugin.yaml
new file mode 100644
index 0000000..c58b407
--- /dev/null
+++ b/golemcore/supabase/plugin.yaml
@@ -0,0 +1,12 @@
+id: golemcore/supabase
+provider: golemcore
+name: supabase
+version: 1.0.0
+pluginApiVersion: 1
+engineVersion: ">=0.0.0 <1.0.0"
+entrypoint: me.golemcore.plugins.golemcore.supabase.SupabasePluginBootstrap
+description: Supabase PostgREST rows plugin backed by the Supabase REST API.
+sourceUrl: https://github.com/alexk-dev/golemcore-plugins/tree/main/golemcore/supabase
+license: Apache-2.0
+maintainers:
+ - alexk-dev
diff --git a/golemcore/supabase/pom.xml b/golemcore/supabase/pom.xml
new file mode 100644
index 0000000..b38f00c
--- /dev/null
+++ b/golemcore/supabase/pom.xml
@@ -0,0 +1,104 @@
+
+
+ 4.0.0
+
+
+ me.golemcore.plugins
+ golemcore-plugins
+ 1.0.0
+ ../../pom.xml
+
+
+ 1.0.0
+ golemcore-supabase-plugin
+ golemcore/supabase
+ Supabase PostgREST rows plugin for GolemCore
+
+
+ golemcore
+ supabase
+ ../../misc/formatter_eclipse.xml
+
+
+
+
+ me.golemcore.plugins
+ golemcore-plugin-extension-api
+ provided
+
+
+ me.golemcore.plugins
+ golemcore-plugin-runtime-api
+ provided
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+ org.springframework
+ spring-context
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.squareup.okhttp3
+ okhttp-jvm
+ ${okhttp.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ src/main/resources
+
+
+ ${project.basedir}
+
+ plugin.yaml
+
+ META-INF/golemcore
+
+
+
+
+ net.revelc.code.formatter
+ formatter-maven-plugin
+
+
+ maven-shade-plugin
+
+
+ maven-antrun-plugin
+
+
+ copy-plugin-artifact
+ package
+
+
+
+
+
+
+
+ run
+
+
+
+
+
+
+
diff --git a/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginBootstrap.java b/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginBootstrap.java
new file mode 100644
index 0000000..f17eb9d
--- /dev/null
+++ b/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginBootstrap.java
@@ -0,0 +1,22 @@
+package me.golemcore.plugins.golemcore.supabase;
+
+import me.golemcore.plugin.api.extension.spi.PluginBootstrap;
+import me.golemcore.plugin.api.extension.spi.PluginDescriptor;
+
+public class SupabasePluginBootstrap implements PluginBootstrap {
+
+ @Override
+ public PluginDescriptor descriptor() {
+ return PluginDescriptor.builder()
+ .id("golemcore/supabase")
+ .provider("golemcore")
+ .name("supabase")
+ .entrypoint(getClass().getName())
+ .build();
+ }
+
+ @Override
+ public Class> configurationClass() {
+ return SupabasePluginConfiguration.class;
+ }
+}
diff --git a/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginConfig.java b/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginConfig.java
new file mode 100644
index 0000000..19b7309
--- /dev/null
+++ b/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginConfig.java
@@ -0,0 +1,84 @@
+package me.golemcore.plugins.golemcore.supabase;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class SupabasePluginConfig {
+
+ static final String DEFAULT_SCHEMA = "public";
+ static final String DEFAULT_SELECT = "*";
+ static final int DEFAULT_LIMIT = 20;
+ static final int MAX_LIMIT = 1000;
+
+ @Builder.Default
+ private Boolean enabled = false;
+
+ private String projectUrl;
+ private String apiKey;
+
+ @Builder.Default
+ private String defaultSchema = DEFAULT_SCHEMA;
+
+ @Builder.Default
+ private String defaultTable = "";
+
+ @Builder.Default
+ private String defaultSelect = DEFAULT_SELECT;
+
+ @Builder.Default
+ private Integer defaultLimit = DEFAULT_LIMIT;
+
+ @Builder.Default
+ private Boolean allowWrite = false;
+
+ @Builder.Default
+ private Boolean allowDelete = false;
+
+ public void normalize() {
+ if (enabled == null) {
+ enabled = false;
+ }
+ projectUrl = normalizeUrl(projectUrl, null);
+ apiKey = normalizeText(apiKey, null);
+ defaultSchema = normalizeText(defaultSchema, DEFAULT_SCHEMA);
+ defaultTable = normalizeText(defaultTable, "");
+ defaultSelect = normalizeText(defaultSelect, DEFAULT_SELECT);
+ if (defaultLimit == null || defaultLimit <= 0) {
+ defaultLimit = DEFAULT_LIMIT;
+ }
+ if (defaultLimit > MAX_LIMIT) {
+ defaultLimit = MAX_LIMIT;
+ }
+ if (allowWrite == null) {
+ allowWrite = false;
+ }
+ if (allowDelete == null) {
+ allowDelete = false;
+ }
+ }
+
+ private String normalizeUrl(String value, String defaultValue) {
+ String trimmed = normalizeText(value, defaultValue);
+ if (trimmed == null) {
+ return null;
+ }
+ while (trimmed.endsWith("/")) {
+ trimmed = trimmed.substring(0, trimmed.length() - 1);
+ }
+ return trimmed.isBlank() ? defaultValue : trimmed;
+ }
+
+ private String normalizeText(String value, String defaultValue) {
+ if (value == null) {
+ return defaultValue;
+ }
+ String trimmed = value.trim();
+ return trimmed.isEmpty() ? defaultValue : trimmed;
+ }
+}
diff --git a/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginConfigService.java b/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginConfigService.java
new file mode 100644
index 0000000..c84ae5f
--- /dev/null
+++ b/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginConfigService.java
@@ -0,0 +1,36 @@
+package me.golemcore.plugins.golemcore.supabase;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import me.golemcore.plugin.api.runtime.PluginConfigurationService;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+@Service
+public class SupabasePluginConfigService {
+
+ static final String PLUGIN_ID = "golemcore/supabase";
+
+ private final PluginConfigurationService pluginConfigurationService;
+ private final ObjectMapper objectMapper;
+
+ public SupabasePluginConfigService(PluginConfigurationService pluginConfigurationService) {
+ this.pluginConfigurationService = pluginConfigurationService;
+ this.objectMapper = new ObjectMapper();
+ }
+
+ public SupabasePluginConfig getConfig() {
+ Map raw = pluginConfigurationService.getPluginConfig(PLUGIN_ID);
+ SupabasePluginConfig config = raw.isEmpty()
+ ? SupabasePluginConfig.builder().build()
+ : objectMapper.convertValue(raw, SupabasePluginConfig.class);
+ config.normalize();
+ return config;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void save(SupabasePluginConfig config) {
+ config.normalize();
+ pluginConfigurationService.savePluginConfig(PLUGIN_ID, objectMapper.convertValue(config, Map.class));
+ }
+}
diff --git a/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginConfiguration.java b/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginConfiguration.java
new file mode 100644
index 0000000..f5b834a
--- /dev/null
+++ b/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginConfiguration.java
@@ -0,0 +1,12 @@
+package me.golemcore.plugins.golemcore.supabase;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration(proxyBeanMethods = false)
+@ComponentScan(basePackageClasses = {
+ SupabaseRowsToolProvider.class,
+ SupabasePluginSettingsContributor.class
+})
+public class SupabasePluginConfiguration {
+}
diff --git a/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginSettingsContributor.java b/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginSettingsContributor.java
new file mode 100644
index 0000000..4c04d95
--- /dev/null
+++ b/golemcore/supabase/src/main/java/me/golemcore/plugins/golemcore/supabase/SupabasePluginSettingsContributor.java
@@ -0,0 +1,261 @@
+package me.golemcore.plugins.golemcore.supabase;
+
+import me.golemcore.plugin.api.extension.spi.PluginActionResult;
+import me.golemcore.plugin.api.extension.spi.PluginSettingsAction;
+import me.golemcore.plugin.api.extension.spi.PluginSettingsCatalogItem;
+import me.golemcore.plugin.api.extension.spi.PluginSettingsContributor;
+import me.golemcore.plugin.api.extension.spi.PluginSettingsField;
+import me.golemcore.plugin.api.extension.spi.PluginSettingsSection;
+import me.golemcore.plugins.golemcore.supabase.support.SupabaseApiClient;
+import me.golemcore.plugins.golemcore.supabase.support.SupabaseApiException;
+import me.golemcore.plugins.golemcore.supabase.support.SupabaseTransportException;
+import org.springframework.stereotype.Component;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+@Component
+public class SupabasePluginSettingsContributor implements PluginSettingsContributor {
+
+ private static final String SECTION_KEY = "main";
+ private static final String ACTION_TEST_CONNECTION = "test-connection";
+
+ private final SupabasePluginConfigService configService;
+ private final SupabaseApiClient apiClient;
+
+ public SupabasePluginSettingsContributor(
+ SupabasePluginConfigService configService,
+ SupabaseApiClient apiClient) {
+ this.configService = configService;
+ this.apiClient = apiClient;
+ }
+
+ @Override
+ public String getPluginId() {
+ return SupabasePluginConfigService.PLUGIN_ID;
+ }
+
+ @Override
+ public List getCatalogItems() {
+ return List.of(PluginSettingsCatalogItem.builder()
+ .pluginId(SupabasePluginConfigService.PLUGIN_ID)
+ .pluginName("supabase")
+ .provider("golemcore")
+ .sectionKey(SECTION_KEY)
+ .title("Supabase")
+ .description("Supabase project, schema, and write permissions for table row operations.")
+ .blockKey("tools")
+ .blockTitle("Tools")
+ .blockDescription("Tool-specific runtime behavior and integrations")
+ .order(34)
+ .build());
+ }
+
+ @Override
+ public PluginSettingsSection getSection(String sectionKey) {
+ requireSection(sectionKey);
+ SupabasePluginConfig config = configService.getConfig();
+ Map values = new LinkedHashMap<>();
+ values.put("enabled", Boolean.TRUE.equals(config.getEnabled()));
+ values.put("projectUrl", orEmpty(config.getProjectUrl()));
+ values.put("apiKey", "");
+ values.put("defaultSchema", config.getDefaultSchema());
+ values.put("defaultTable", config.getDefaultTable());
+ values.put("defaultSelect", config.getDefaultSelect());
+ values.put("defaultLimit", config.getDefaultLimit());
+ values.put("allowWrite", Boolean.TRUE.equals(config.getAllowWrite()));
+ values.put("allowDelete", Boolean.TRUE.equals(config.getAllowDelete()));
+ return PluginSettingsSection.builder()
+ .title("Supabase")
+ .description("Configure Supabase PostgREST access for listing and mutating table rows.")
+ .fields(List.of(
+ booleanField("enabled", "Enable Supabase",
+ "Allow the model to query and mutate Supabase rows."),
+ textField("projectUrl", "Project URL",
+ "Supabase project URL, for example https://your-project.supabase.co.",
+ "https://your-project.supabase.co"),
+ secretField("apiKey", "API Key",
+ "Supabase service role or anon key. Leave blank to keep the current secret."),
+ textField("defaultSchema", "Default Schema",
+ "Default Postgres schema used for requests.", SupabasePluginConfig.DEFAULT_SCHEMA),
+ textField("defaultTable", "Default Table",
+ "Optional default table name used when the tool call omits table.",
+ "tasks"),
+ textField("defaultSelect", "Default Select",
+ "Default PostgREST select expression for row queries.",
+ SupabasePluginConfig.DEFAULT_SELECT),
+ numberField("defaultLimit", "Default Limit",
+ "Maximum rows returned when limit is omitted.", 1.0, 1000.0, 1.0),
+ booleanField("allowWrite", "Allow Write",
+ "Permit insert_row and update_rows operations."),
+ booleanField("allowDelete", "Allow Delete",
+ "Permit delete_rows operations.")))
+ .values(values)
+ .actions(List.of(PluginSettingsAction.builder()
+ .actionId(ACTION_TEST_CONNECTION)
+ .label("Test Connection")
+ .variant("secondary")
+ .build()))
+ .build();
+ }
+
+ @Override
+ public PluginSettingsSection saveSection(String sectionKey, Map values) {
+ requireSection(sectionKey);
+ SupabasePluginConfig config = configService.getConfig();
+ config.setEnabled(readBoolean(values, "enabled", false));
+ config.setProjectUrl(readString(values, "projectUrl", config.getProjectUrl()));
+ String apiKey = readString(values, "apiKey", null);
+ if (apiKey != null && !apiKey.isBlank()) {
+ config.setApiKey(apiKey);
+ }
+ config.setDefaultSchema(readString(values, "defaultSchema", config.getDefaultSchema()));
+ config.setDefaultTable(readString(values, "defaultTable", config.getDefaultTable()));
+ config.setDefaultSelect(readString(values, "defaultSelect", config.getDefaultSelect()));
+ config.setDefaultLimit(readInteger(values, "defaultLimit", config.getDefaultLimit()));
+ config.setAllowWrite(readBoolean(values, "allowWrite", false));
+ config.setAllowDelete(readBoolean(values, "allowDelete", false));
+ configService.save(config);
+ return getSection(sectionKey);
+ }
+
+ @Override
+ public PluginActionResult executeAction(String sectionKey, String actionId, Map payload) {
+ requireSection(sectionKey);
+ if (ACTION_TEST_CONNECTION.equals(actionId)) {
+ return testConnection();
+ }
+ throw new IllegalArgumentException("Unknown Supabase action: " + actionId);
+ }
+
+ private PluginActionResult testConnection() {
+ SupabasePluginConfig config = configService.getConfig();
+ if (!hasText(config.getProjectUrl())) {
+ return PluginActionResult.builder()
+ .status("error")
+ .message("Supabase project URL is not configured.")
+ .build();
+ }
+ if (!hasText(config.getApiKey())) {
+ return PluginActionResult.builder()
+ .status("error")
+ .message("Supabase API key is not configured.")
+ .build();
+ }
+ if (!hasText(config.getDefaultTable())) {
+ return PluginActionResult.builder()
+ .status("error")
+ .message("Default Supabase table is not configured.")
+ .build();
+ }
+ try {
+ List